diff --git a/build/public/assets/index.8f0efd0f.css b/build/public/assets/index.22dc307c.css similarity index 51% rename from build/public/assets/index.8f0efd0f.css rename to build/public/assets/index.22dc307c.css index 8d26cf8..b986684 100644 --- a/build/public/assets/index.8f0efd0f.css +++ b/build/public/assets/index.22dc307c.css @@ -1 +1 @@ -:root{--main-color:#c1b19f;--main-darker-color:#4f4e4c;--link-color:#808db0;--link-hover-color:#c5cfeb;--highlight-color:#dd7e7e;--positive-color:#64a756;--input-bg-color:#262523;--bg-color:rgba(0,0,0,.7)}body,html{margin:0;background:#2b2b2b;color:var(--main-color);height:100%}*{font-family:monospace;font-size:15px}h1,h2,h3,h4{font-size:20px}a{color:var(--link-color);text-decoration:none}a:hover{color:var(--link-hover-color)}td,th{vertical-align:top}.btn{display:inline-block;background:var(--input-bg-color);color:var(--link-color);border:solid 1px #000;padding:5px 10px;box-shadow:1px 1px 2px rgba(0,0,0,.5),0 0 1px rgba(150,150,150,.4) inset;border-radius:4px;user-select:none}.btn-big{font-size:1.5em;padding:10px 20px}.btn:hover{background:#2f2e2c;color:var(--link-hover-color);border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:pointer}.btn:disabled{background:#2f2e2c;color:#8c4747!important;border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:not-allowed}input{background:#333230;border-radius:4px;color:var(--main-color);padding:6px 10px;border:solid 1px #000;box-shadow:0 0 3px rgba(0,0,0,.3) inset}input:focus{border:solid 1px #686767;background:var(--input-bg-color)}.scores{position:absolute;right:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.timer{position:absolute;left:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.menu{position:absolute;top:0;left:50%;transform:translateX(-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:2}.closed{display:none}.overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:10;background:var(--bg-color)}.overlay.transparent{background:0 0}.overlay-content{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:1}.connection-lost .overlay-content{padding:20px;text-align:center}.preview{position:absolute;top:20px;left:20px;bottom:20px;right:20px}.preview .img{height:100%;width:100%;position:absolute;background-repeat:no-repeat;background-position:center;background-size:contain}.menu .opener{display:inline-block;margin-right:10px;color:var(--link-color)}.menu .opener:last-child{margin-right:0}.menu .opener:hover{color:var(--link-hover-color);cursor:pointer}kbd{background-color:#eee;border-radius:3px;border:1px solid #b4b4b4;box-shadow:0 1px 1px rgba(0,0,0,.2),0 2px 0 0 rgba(255,255,255,.7) inset;color:#333;display:inline-block;font-size:.85em;font-weight:700;line-height:1;padding:2px 4px;white-space:nowrap}.hint{color:var(--main-darker-color)}.bit{background:#3b3737;border-radius:.5em;padding:.25em .5em;display:inline-block;margin:0 .25em .25em 0;cursor:pointer}.bit.on{color:var(--positive-color)}.upload-image-teaser{text-align:center}.upload-image-teaser .btn{margin-bottom:.5em}table label{line-height:32px}.nav{list-style:none;padding:0}.nav li{display:inline-block;margin-right:1em}.image-list{overflow:scroll}.image-list-inner{white-space:nowrap}.imageteaser{width:150px;height:100px;display:inline-block;margin:5px;background-size:contain;background-position:center;background-repeat:no-repeat;background-color:#222;cursor:pointer}.game-teaser-wrap{display:inline-block;width:20%;padding:5px;box-sizing:border-box}.game-teaser{display:block;background-repeat:no-repeat;background-position:center;background-size:contain;position:relative;padding-top:56.25%;width:100%;background-color:#222}.game-info{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%}.game-info-text{position:absolute;top:0;background:var(--bg-color);padding:5px}.game-replay{position:absolute;top:0;right:0}html.view-game{overflow:hidden}html.view-game body{overflow:hidden}html.view-replay{overflow:hidden}html.view-replay body{overflow:hidden}.imageteaser{position:relative}.imageteaser .edit{display:none;position:absolute}.imageteaser:hover .edit{display:inline-block}.input[data-v-39ed99c7]{margin-bottom:.5em}.autocomplete[data-v-39ed99c7]{position:relative}.autocomplete ul[data-v-39ed99c7]{list-style:none;padding:0;margin:0;position:absolute;left:0;right:0;background:#333230;top:-.5em}.autocomplete ul li[data-v-39ed99c7]{position:relative;padding:.5em .5em .5em 1.5em;cursor:pointer}.autocomplete ul li.active[data-v-39ed99c7]{color:var(--link-hover-color);background:var(--input-bg-color)}.autocomplete ul li.active[data-v-39ed99c7]:before{content:'β–Ά';display:block;position:absolute;left:.5em}.new-image-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}@media (max-width:1400px){.new-image-dialog .overlay-content{grid-template-columns:auto;grid-template-rows:1fr min-content min-content;grid-template-areas:"image" "settings" "buttons"}.new-image-dialog .overlay-content .area-buttons .btn br{display:none}}.new-image-dialog .area-image{grid-area:image;margin:.5em;border:solid 6px transparent}.new-image-dialog .area-image.no-image{align-content:center;display:grid;text-align:center;border:solid 6px;position:relative}.new-image-dialog .area-image.droppable{border:dashed 6px}.area-image *{pointer-events:none}.new-image-dialog .area-image .has-image{position:relative;width:100%;height:100%}.new-image-dialog .area-image .has-image .remove{position:absolute;top:.5em;left:.5em}.new-image-dialog .area-settings{grid-area:settings}.new-image-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-image-dialog .area-buttons{align-self:end;grid-area:buttons}.new-image-dialog .area-buttons button{width:100%;margin-top:.5em}.new-image-dialog .upload{position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer}.new-image-dialog .upload .btn{position:absolute;top:50%;transform:translate(-50%,-50%)}.edit-image-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}.edit-image-dialog .area-image{grid-area:image;margin:20px}.edit-image-dialog .area-image.no-image{align-content:center;display:grid;text-align:center;border:dashed 6px;position:relative}.edit-image-dialog .area-image .has-image{position:relative;width:100%;height:100%}.edit-image-dialog .area-image .has-image .remove{position:absolute;top:.5em;left:.5em}.edit-image-dialog .area-settings{grid-area:settings}.edit-image-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.edit-image-dialog .area-buttons{align-self:end;grid-area:buttons}.edit-image-dialog .area-buttons button{width:100%;margin-top:.5em}.new-game-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}.new-game-dialog .area-image{grid-area:image;display:grid;grid-template-rows:1fr min-content;grid-template-areas:"image" "image-title";margin-right:1em}@media (max-width:1400px){.new-game-dialog .overlay-content{grid-template-columns:auto;grid-template-rows:1fr min-content min-content;grid-template-areas:"image" "settings" "buttons"}.new-game-dialog .area-image{margin-right:0}}.new-game-dialog .area-settings{grid-area:settings}.new-game-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-game-dialog .area-buttons{align-self:end;grid-area:buttons}.new-game-dialog .area-buttons button{width:100%}.new-game-dialog .has-image{box-sizing:border-box;grid-area:image;position:relative;width:100%;height:100%;border:solid 1px}.new-game-dialog .image-title{grid-area:image-title;text-align:center;padding:.5em 0;background:var(--main-color);color:#262523}.new-game-dialog .has-image .remove{position:absolute;top:.5em;left:.5em} \ No newline at end of file +:root{--main-color:#c1b19f;--main-darker-color:#4f4e4c;--link-color:#808db0;--link-hover-color:#c5cfeb;--highlight-color:#dd7e7e;--positive-color:#64a756;--input-bg-color:#262523;--bg-color:rgba(0,0,0,.7)}body,html{margin:0;background:#2b2b2b;color:var(--main-color);height:100%}*{font-family:monospace;font-size:15px}h1,h2,h3,h4{font-size:20px}a{color:var(--link-color);text-decoration:none}a:hover{color:var(--link-hover-color)}td,th{vertical-align:top}.btn{display:inline-block;background:var(--input-bg-color);color:var(--link-color);border:solid 1px #000;padding:5px 10px;box-shadow:1px 1px 2px rgba(0,0,0,.5),0 0 1px rgba(150,150,150,.4) inset;border-radius:4px;user-select:none}.btn-big{font-size:1.5em;padding:10px 20px}.btn:hover{background:#2f2e2c;color:var(--link-hover-color);border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:pointer}.btn:disabled{background:#2f2e2c;color:#8c4747!important;border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:not-allowed}input{background:#333230;border-radius:4px;color:var(--main-color);padding:6px 10px;border:solid 1px #000;box-shadow:0 0 3px rgba(0,0,0,.3) inset}input:focus{border:solid 1px #686767;background:var(--input-bg-color)}.scores{position:absolute;right:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.timer{position:absolute;left:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.menu{position:absolute;top:0;left:50%;transform:translateX(-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:2}.closed{display:none}.overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:10;background:var(--bg-color)}.overlay.transparent{background:0 0}.overlay-content{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:1}.connection-lost .overlay-content{padding:20px;text-align:center}.preview{position:absolute;top:20px;left:20px;bottom:20px;right:20px}.preview .img{height:100%;width:100%;position:absolute;background-repeat:no-repeat;background-position:center;background-size:contain}.menu .opener{display:inline-block;margin-right:10px;color:var(--link-color)}.menu .opener:last-child{margin-right:0}.menu .opener:hover{color:var(--link-hover-color);cursor:pointer}kbd{background-color:#eee;border-radius:3px;border:1px solid #b4b4b4;box-shadow:0 1px 1px rgba(0,0,0,.2),0 2px 0 0 rgba(255,255,255,.7) inset;color:#333;display:inline-block;font-size:.85em;font-weight:700;line-height:1;padding:2px 4px;white-space:nowrap}.hint{color:var(--main-darker-color)}.bit{background:#3b3737;border-radius:.5em;padding:.25em .5em;display:inline-block;margin:0 .25em .25em 0;cursor:pointer}.bit.on{color:var(--positive-color)}.upload-image-teaser{text-align:center}.upload-image-teaser .btn{margin-bottom:.5em}table label{line-height:32px}.nav{list-style:none;padding:0}.nav li{display:inline-block;margin-right:1em}.image-list{overflow:scroll}.image-list-inner{white-space:nowrap}.imageteaser{width:150px;height:100px;display:inline-block;margin:5px;background-size:contain;background-position:center;background-repeat:no-repeat;background-color:#222;cursor:pointer}.game-teaser-wrap{display:inline-block;width:20%;padding:5px;box-sizing:border-box}.game-teaser{display:block;background-repeat:no-repeat;background-position:center;background-size:contain;position:relative;padding-top:56.25%;width:100%;background-color:#222}.game-info{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%}.game-info-text{position:absolute;top:0;background:var(--bg-color);padding:5px}.game-replay{position:absolute;top:0;right:0}html.view-game{overflow:hidden}html.view-game body{overflow:hidden}html.view-replay{overflow:hidden}html.view-replay body{overflow:hidden}.imageteaser{position:relative}.imageteaser .edit{display:none;position:absolute}.imageteaser:hover .edit{display:inline-block}.input[data-v-a4fa5e7e]{margin-bottom:.5em}.autocomplete[data-v-a4fa5e7e]{position:relative}.autocomplete ul[data-v-a4fa5e7e]{list-style:none;padding:0;margin:0;position:absolute;left:0;right:0;background:#333230;top:-.5em}.autocomplete ul li[data-v-a4fa5e7e]{position:relative;padding:.5em .5em .5em 1.5em;cursor:pointer}.autocomplete ul li.active[data-v-a4fa5e7e]{color:var(--link-hover-color);background:var(--input-bg-color)}.autocomplete ul li.active[data-v-a4fa5e7e]:before{content:'β–Ά';display:block;position:absolute;left:.5em}.new-image-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}@media (max-width:1400px) and (min-height:720px),(max-width:1000px){.new-image-dialog .overlay-content{grid-template-columns:auto;grid-template-rows:1fr min-content min-content;grid-template-areas:"image" "settings" "buttons"}.new-image-dialog .overlay-content .area-buttons .btn br{display:none}}.new-image-dialog .area-image{grid-area:image;margin:.5em;border:solid 6px transparent}.new-image-dialog .area-image.no-image{align-content:center;display:grid;text-align:center;border:solid 6px;position:relative}.new-image-dialog .area-image.droppable{border:dashed 6px}.new-image-dialog .area-image .has-image{position:relative;width:100%;height:100%}.new-image-dialog .area-image .has-image .remove{position:absolute;top:.5em;left:.5em}.new-image-dialog .area-settings{grid-area:settings}.new-image-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-image-dialog .area-buttons{align-self:end;grid-area:buttons}.new-image-dialog .area-buttons button{width:100%;margin-top:.5em}.new-image-dialog .upload{position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer}.new-image-dialog .upload .btn{position:absolute;top:50%;transform:translate(-50%,-50%)}.area-image .drop-target{display:none}.area-image.droppable .drop-target{pointer-events:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:3}.edit-image-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}@media (max-width:1400px) and (min-height:720px),(max-width:1000px){.edit-image-dialog .overlay-content{grid-template-columns:auto;grid-template-rows:1fr min-content min-content;grid-template-areas:"image" "settings" "buttons"}}.edit-image-dialog .area-image{grid-area:image;margin:20px}.edit-image-dialog .area-image.no-image{align-content:center;display:grid;text-align:center;border:dashed 6px;position:relative}.edit-image-dialog .area-image .has-image{position:relative;width:100%;height:100%}.edit-image-dialog .area-image .has-image .remove{position:absolute;top:.5em;left:.5em}.edit-image-dialog .area-settings{grid-area:settings}.edit-image-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.edit-image-dialog .area-buttons{align-self:end;grid-area:buttons}.edit-image-dialog .area-buttons button{width:100%;margin-top:.5em}.new-game-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}.new-game-dialog .area-image{grid-area:image;display:grid;grid-template-rows:1fr min-content;grid-template-areas:"image" "image-title";margin-right:1em}@media (max-width:1400px) and (min-height:720px),(max-width:1000px){.new-game-dialog .overlay-content{grid-template-columns:auto;grid-template-rows:1fr min-content min-content;grid-template-areas:"image" "settings" "buttons"}.new-game-dialog .area-image{margin-right:0}}.new-game-dialog .area-settings{grid-area:settings}.new-game-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-game-dialog .area-buttons{align-self:end;grid-area:buttons}.new-game-dialog .area-buttons button{width:100%}.new-game-dialog .has-image{box-sizing:border-box;grid-area:image;position:relative;width:100%;height:100%;border:solid 1px}.new-game-dialog .image-title{grid-area:image-title;text-align:center;padding:.5em 0;background:var(--main-color);color:#262523}.new-game-dialog .has-image .remove{position:absolute;top:.5em;left:.5em}.new-game-dialog .image-title>span{margin-right:.5em}.new-game-dialog .image-title>span:last-child{margin-right:0}.image-title-dim{display:inline-block;white-space:no-wrap}.sound-volume span[data-v-4d56fc17]{cursor:pointer;user-select:none}.sound-volume input[data-v-4d56fc17]{vertical-align:middle} \ No newline at end of file diff --git a/build/public/assets/index.63ff8630.js b/build/public/assets/index.63ff8630.js new file mode 100644 index 0000000..bb8b554 --- /dev/null +++ b/build/public/assets/index.63ff8630.js @@ -0,0 +1 @@ +import{d as e,c as t,a as n,w as o,b as l,r as a,o as s,e as i,t as r,F as d,f as c,g as u,h as p,v as g,i as h,j as m,p as y,k as f,l as v,m as w,n as b,q as C,s as x,u as k,x as P,y as A}from"./vendor.684f7bc8.js";var S=e({name:"app",computed:{showNav(){return!["game","replay"].includes(String(this.$route.name))}}});const z={id:"app"},T={key:0,class:"nav"},I=i("Games overview"),E=i("New game");S.render=function(e,i,r,d,c,u){const p=a("router-link"),g=a("router-view");return s(),t("div",z,[e.showNav?(s(),t("ul",T,[n("li",null,[n(p,{class:"btn",to:{name:"index"}},{default:o((()=>[I])),_:1})]),n("li",null,[n(p,{class:"btn",to:{name:"new-game"}},{default:o((()=>[E])),_:1})])])):l("",!0),n(g)])};let M="",D="";const N=async(e,t,n)=>new Promise(((o,l)=>{const a=new window.XMLHttpRequest;a.open(e,t,!0),a.withCredentials=!0;for(const e in n.headers||{})a.setRequestHeader(e,n.headers[e]);a.setRequestHeader("Client-Id",M),a.setRequestHeader("Client-Secret",D),a.addEventListener("load",(function(e){o({status:this.status,text:this.responseText,json:async()=>JSON.parse(this.responseText)})})),a.addEventListener("error",(function(e){l(new Error("xhr error"))})),a.upload&&n.onUploadProgress&&a.upload.addEventListener("progress",(function(e){n.onUploadProgress&&n.onUploadProgress(e)})),a.send(n.body||null)}));var _=(e,t)=>N("get",e,t),V=(e,t)=>N("post",e,t),O=e=>{M=e},B=e=>{D=e};const U=864e5,R=e=>{const t=Math.floor(e/U);e%=U;const n=Math.floor(e/36e5);e%=36e5;const o=Math.floor(e/6e4);e%=6e4;return`${t}d ${n}h ${o}m ${Math.floor(e/1e3)}s`};var $=1,G=1e3,L=()=>{const e=new Date;return Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())},F=(e,t)=>R(t-e),j=R,W=e({name:"game-teaser",props:{game:{type:Object,required:!0}},computed:{style(){return{"background-image":`url("${this.game.imageUrl.replace("uploads/","uploads/r/")+"-375x210.webp"}")`}}},methods:{time(e,t){const n=t?"🏁":"⏳",o=e,l=t||L();return`${n} ${F(o,l)}`}}});const H={class:"game-info-text"},K=n("br",null,null,-1),q=n("br",null,null,-1),Y=n("br",null,null,-1),Q=i(" β†ͺ️ Watch replay ");W.render=function(e,d,c,u,p,g){const h=a("router-link");return s(),t("div",{class:"game-teaser",style:e.style},[n(h,{class:"game-info",to:{name:"game",params:{id:e.game.id}}},{default:o((()=>[n("span",H,[i(" 🧩 "+r(e.game.tilesFinished)+"/"+r(e.game.tilesTotal),1),K,i(" πŸ‘₯ "+r(e.game.players),1),q,i(" "+r(e.time(e.game.started,e.game.finished)),1),Y])])),_:1},8,["to"]),e.game.hasReplay?(s(),t(h,{key:0,class:"game-replay",to:{name:"replay",params:{id:e.game.id}}},{default:o((()=>[Q])),_:1},8,["to"])):l("",!0)],4)};var Z=e({components:{GameTeaser:W},data:()=>({gamesRunning:[],gamesFinished:[]}),async created(){const e=await _("/api/index-data",{}),t=await e.json();this.gamesRunning=t.gamesRunning,this.gamesFinished=t.gamesFinished}});const X=n("h1",null,"Running games",-1),J=n("h1",null,"Finished games",-1);Z.render=function(e,o,l,i,r,u){const p=a("game-teaser");return s(),t("div",null,[X,(s(!0),t(d,null,c(e.gamesRunning,((e,o)=>(s(),t("div",{class:"game-teaser-wrap",key:o},[n(p,{game:e},null,8,["game"])])))),128)),J,(s(!0),t(d,null,c(e.gamesFinished,((e,o)=>(s(),t("div",{class:"game-teaser-wrap",key:o},[n(p,{game:e},null,8,["game"])])))),128))])};var ee=e({name:"image-teaser",props:{image:{type:Object,required:!0}},computed:{style(){return{backgroundImage:`url("${this.image.url.replace("uploads/","uploads/r/")+"-150x100.webp"}")`}},canEdit(){return!!this.$me.id&&this.$me.id===this.image.uploaderUserId}},emits:{click:null,editClick:null},methods:{onClick(){this.$emit("click")},onEditClick(){this.$emit("editClick")}}});ee.render=function(e,n,o,a,i,r){return s(),t("div",{class:"imageteaser",style:e.style,onClick:n[2]||(n[2]=(...t)=>e.onClick&&e.onClick(...t))},[e.canEdit?(s(),t("div",{key:0,class:"btn edit",onClick:n[1]||(n[1]=u(((...t)=>e.onEditClick&&e.onEditClick(...t)),["stop"]))},"✏️")):l("",!0)],4)};var te=e({name:"image-library",components:{ImageTeaser:ee},props:{images:{type:Array,required:!0}},emits:{imageClicked:null,imageEditClicked:null},methods:{imageClicked(e){this.$emit("imageClicked",e)},imageEditClicked(e){this.$emit("imageEditClicked",e)}}});te.render=function(e,n,o,l,i,r){const u=a("image-teaser");return s(),t("div",null,[(s(!0),t(d,null,c(e.images,((n,o)=>(s(),t(u,{image:n,onClick:t=>e.imageClicked(n),onEditClick:t=>e.imageEditClicked(n),key:o},null,8,["image","onClick","onEditClick"])))),128))])};class ne{constructor(e){this.rand_high=e||3735929054,this.rand_low=1231121986^e}random(e,t){this.rand_high=(this.rand_high<<16)+(this.rand_high>>16)+this.rand_low&4294967295,this.rand_low=this.rand_low+this.rand_high&4294967295;return e+(this.rand_high>>>0)/4294967295*(t-e+1)|0}choice(e){return e[this.random(0,e.length-1)]}shuffle(e){const t=e.slice();for(let n=0;n<=t.length-2;n++){const e=this.random(n,t.length-1),o=t[n];t[n]=t[e],t[e]=o}return t}static serialize(e){return{rand_high:e.rand_high,rand_low:e.rand_low}}static unserialize(e){const t=new ne(0);return t.rand_high=e.rand_high,t.rand_low=e.rand_low,t}}const oe=(e,t)=>{const n=`${e}`;return n.length>=t.length?n:t.substr(0,t.length-n.length)+n},le=(...e)=>{const t=t=>(...n)=>{const o=new Date,l=oe(o.getHours(),"00"),a=oe(o.getMinutes(),"00"),s=oe(o.getSeconds(),"00");console[t](`${l}:${a}:${s}`,...e,...n)};return{log:t("log"),error:t("error"),info:t("info")}};var ae={hash:e=>{let t=0;for(let n=0;n{let t=e.toLowerCase();return t=t.replace(/[^a-z0-9]+/g,"-"),t=t.replace(/^-|-$/,""),t},uniqId:()=>Date.now().toString(36)+Math.random().toString(36).substring(2),encodeShape:function(e){return e.top+1<<0|e.right+1<<2|e.bottom+1<<4|e.left+1<<6},decodeShape:function(e){return{top:(e>>0&3)-1,right:(e>>2&3)-1,bottom:(e>>4&3)-1,left:(e>>6&3)-1}},encodePiece:function(e){return[e.idx,e.pos.x,e.pos.y,e.z,e.owner,e.group]},decodePiece:function(e){return{idx:e[0],pos:{x:e[1],y:e[2]},z:e[3],owner:e[4],group:e[5]}},encodePlayer:function(e){return[e.id,e.x,e.y,e.d,e.name,e.color,e.bgcolor,e.points,e.ts]},decodePlayer:function(e){return{id:e[0],x:e[1],y:e[2],d:e[3],name:e[4],color:e[5],bgcolor:e[6],points:e[7],ts:e[8]}},encodeGame:function(e){return[e.id,e.rng.type||"",ne.serialize(e.rng.obj),e.puzzle,e.players,e.evtInfos,e.scoreMode,e.shapeMode,e.snapMode,e.creatorUserId]},decodeGame:function(e){return{id:e[0],rng:{type:e[1],obj:ne.unserialize(e[2])},puzzle:e[3],players:e[4],evtInfos:e[5],scoreMode:e[6],shapeMode:e[7],snapMode:e[8],creatorUserId:e[9]}},coordByPieceIdx:function(e,t){const n=e.width/e.tileSize;return{x:t%n,y:Math.floor(t/n)}},asQueryArgs:function(e){const t=[];for(const n in e){const o=[n,e[n]].map(encodeURIComponent);t.push(o.join("="))}return 0===t.length?"":`?${t.join("&")}`}};const se={name:"responsive-image",props:{src:String,title:{type:String,default:""},height:{type:String,default:"100%"},width:{type:String,default:"100%"}},computed:{style(){return{display:"inline-block",verticalAlign:"text-bottom",backgroundImage:`url('${this.src}')`,backgroundRepeat:"no-repeat",backgroundSize:"contain",backgroundPosition:"center",width:this.width,height:this.height}}}};se.render=function(e,n,o,l,a,i){return s(),t("div",{style:i.style,title:o.title},null,12,["title"])};var ie=e({name:"tags-input",props:{modelValue:{type:Array,required:!0},autocompleteTags:{type:Function}},emits:{"update:modelValue":null},data:()=>({input:"",values:[],autocomplete:{idx:-1,values:[]}}),created(){this.values=this.modelValue},methods:{onKeyUp(e){return"ArrowDown"===e.code&&this.autocomplete.values.length>0?(this.autocomplete.idx0?(this.autocomplete.idx>0&&this.autocomplete.idx--,e.stopPropagation(),!1):","===e.key?(this.add(),e.stopPropagation(),!1):void(this.input&&this.autocompleteTags?(this.autocomplete.values=this.autocompleteTags(this.input,this.values),this.autocomplete.idx=-1):(this.autocomplete.values=[],this.autocomplete.idx=-1))},addVal(e){const t=e.replace(/,/g,"").trim();t&&(this.values.includes(t)||this.values.push(t),this.input="",this.autocomplete.values=[],this.autocomplete.idx=-1,this.$emit("update:modelValue",this.values),this.$refs.input.focus())},add(){const e=this.autocomplete.idx>=0?this.autocomplete.values[this.autocomplete.idx]:this.input;this.addVal(e)},rm(e){this.values=this.values.filter((t=>t!==e)),this.$emit("update:modelValue",this.values)}}});const re=m();y("data-v-a4fa5e7e");const de={key:0,class:"autocomplete"};f();const ce=re(((e,o,a,i,u,m)=>(s(),t("div",null,[p(n("input",{ref:"input",class:"input",type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.input=t),placeholder:"Plants, People",onChange:o[2]||(o[2]=(...t)=>e.onChange&&e.onChange(...t)),onKeydown:o[3]||(o[3]=h(((...t)=>e.add&&e.add(...t)),["enter"])),onKeyup:o[4]||(o[4]=(...t)=>e.onKeyUp&&e.onKeyUp(...t))},null,544),[[g,e.input]]),e.autocomplete.values?(s(),t("div",de,[n("ul",null,[(s(!0),t(d,null,c(e.autocomplete.values,((n,o)=>(s(),t("li",{key:o,class:{active:o===e.autocomplete.idx},onClick:t=>e.addVal(n)},r(n),11,["onClick"])))),128))])])):l("",!0),(s(!0),t(d,null,c(e.values,((n,o)=>(s(),t("span",{key:o,class:"bit",onClick:t=>e.rm(n)},r(n)+" βœ–",9,["onClick"])))),128))]))));ie.render=ce,ie.__scopeId="data-v-a4fa5e7e";const ue=le("NewImageDialog.vue");var pe=e({name:"new-image-dialog",components:{ResponsiveImage:se,TagsInput:ie},props:{autocompleteTags:{type:Function},uploadProgress:{type:Number},uploading:{type:String}},emits:{bgclick:null,setupGameClick:null,postToGalleryClick:null},data:()=>({previewUrl:"",file:null,title:"",tags:[],droppable:!1}),computed:{uploadProgressPercent(){return this.uploadProgress?Math.round(100*this.uploadProgress):0},canPostToGallery(){return!this.uploading&&!(!this.previewUrl||!this.file)},canSetupGameClick(){return!this.uploading&&!(!this.previewUrl||!this.file)}},methods:{imageFromDragEvt(e){var t;const n=null==(t=e.dataTransfer)?void 0:t.items;if(!n||0===n.length)return null;const o=n[0];return o.type.startsWith("image/")?o:null},onFileSelect(e){const t=e.target;if(!t.files)return;const n=t.files[0];n&&this.preview(n)},preview(e){const t=new FileReader;t.readAsDataURL(e),t.onload=t=>{this.previewUrl=t.target.result,this.file=e}},postToGallery(){this.$emit("postToGalleryClick",{file:this.file,title:this.title,tags:this.tags})},setupGameClick(){this.$emit("setupGameClick",{file:this.file,title:this.title,tags:this.tags})},onDrop(e){this.droppable=!1;const t=this.imageFromDragEvt(e);if(!t)return!1;const n=t.getAsFile();return!!n&&(this.file=n,this.preview(n),e.preventDefault(),!1)},onDragover(e){return!!this.imageFromDragEvt(e)&&(this.droppable=!0,e.preventDefault(),!1)},onDragleave(){ue.info("onDragleave"),this.droppable=!1}}});const ge=n("div",{class:"drop-target"},null,-1),he={key:0,class:"has-image"},me={key:1},ye={class:"upload"},fe=n("span",{class:"btn"},"Upload File",-1),ve={class:"area-settings"},we=n("td",null,[n("label",null,"Title")],-1),be=n("tr",null,[n("td",{colspan:"2"},[n("div",{class:"hint"},"Feel free to leave a credit to the artist/photographer in the title :)")])],-1),Ce=n("td",null,[n("label",null,"Tags")],-1),xe={class:"area-buttons"},ke=i("πŸ–ΌοΈ Post to gallery"),Pe=i("🧩 Post to gallery "),Ae=n("br",null,null,-1),Se=i(" + set up game");pe.render=function(e,o,l,c,h,m){const y=a("responsive-image"),f=a("tags-input");return s(),t("div",{class:"overlay new-image-dialog",onClick:o[11]||(o[11]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[10]||(o[10]=u((()=>{}),["stop"]))},[n("div",{class:["area-image",{"has-image":!!e.previewUrl,"no-image":!e.previewUrl,droppable:e.droppable}],onDrop:o[3]||(o[3]=(...t)=>e.onDrop&&e.onDrop(...t)),onDragover:o[4]||(o[4]=(...t)=>e.onDragover&&e.onDragover(...t)),onDragleave:o[5]||(o[5]=(...t)=>e.onDragleave&&e.onDragleave(...t))},[ge,e.previewUrl?(s(),t("div",he,[n("span",{class:"remove btn",onClick:o[1]||(o[1]=t=>e.previewUrl="")},"X"),n(y,{src:e.previewUrl},null,8,["src"])])):(s(),t("div",me,[n("label",ye,[n("input",{type:"file",style:{display:"none"},onChange:o[2]||(o[2]=(...t)=>e.onFileSelect&&e.onFileSelect(...t)),accept:"image/*"},null,32),fe])]))],34),n("div",ve,[n("table",null,[n("tr",null,[we,n("td",null,[p(n("input",{type:"text","onUpdate:modelValue":o[6]||(o[6]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[g,e.title]])])]),be,n("tr",null,[Ce,n("td",null,[n(f,{modelValue:e.tags,"onUpdate:modelValue":o[7]||(o[7]=t=>e.tags=t),autocompleteTags:e.autocompleteTags},null,8,["modelValue","autocompleteTags"])])])])]),n("div",xe,[n("button",{class:"btn",disabled:!e.canPostToGallery,onClick:o[8]||(o[8]=(...t)=>e.postToGallery&&e.postToGallery(...t))},["postToGallery"===e.uploading?(s(),t(d,{key:0},[i("Uploading ("+r(e.uploadProgressPercent)+"%)",1)],64)):(s(),t(d,{key:1},[ke],64))],8,["disabled"]),n("button",{class:"btn",disabled:!e.canSetupGameClick,onClick:o[9]||(o[9]=(...t)=>e.setupGameClick&&e.setupGameClick(...t))},["setupGame"===e.uploading?(s(),t(d,{key:0},[i("Uploading ("+r(e.uploadProgressPercent)+"%)",1)],64)):(s(),t(d,{key:1},[Pe,Ae,Se],64))],8,["disabled"])])])])};var ze=e({name:"edit-image-dialog",components:{ResponsiveImage:se,TagsInput:ie},props:{image:{type:Object,required:!0},autocompleteTags:{type:Function}},emits:{bgclick:null,saveClick:null},data:()=>({title:"",tags:[]}),created(){this.title=this.image.title,this.tags=this.image.tags.map((e=>e.title))},methods:{saveImage(){this.$emit("saveClick",{id:this.image.id,title:this.title,tags:this.tags})}}});const Te={class:"area-image"},Ie={class:"has-image"},Ee={class:"area-settings"},Me=n("td",null,[n("label",null,"Title")],-1),De=n("tr",null,[n("td",{colspan:"2"},[n("div",{class:"hint"},"Feel free to leave a credit to the artist/photographer in the title :)")])],-1),Ne=n("td",null,[n("label",null,"Tags")],-1),_e={class:"area-buttons"};var Ve,Oe,Be,Ue,Re,$e,Ge,Le;ze.render=function(e,o,l,i,r,d){const c=a("responsive-image"),h=a("tags-input");return s(),t("div",{class:"overlay edit-image-dialog",onClick:o[5]||(o[5]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[4]||(o[4]=u((()=>{}),["stop"]))},[n("div",Te,[n("div",Ie,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",Ee,[n("table",null,[n("tr",null,[Me,n("td",null,[p(n("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[g,e.title]])])]),De,n("tr",null,[Ne,n("td",null,[n(h,{modelValue:e.tags,"onUpdate:modelValue":o[2]||(o[2]=t=>e.tags=t),autocompleteTags:e.autocompleteTags},null,8,["modelValue","autocompleteTags"])])])])]),n("div",_e,[n("button",{class:"btn",onClick:o[3]||(o[3]=(...t)=>e.saveImage&&e.saveImage(...t))},"πŸ–ΌοΈ Save image")])])])},(Oe=Ve||(Ve={}))[Oe.Flat=0]="Flat",Oe[Oe.Out=1]="Out",Oe[Oe.In=-1]="In",(Ue=Be||(Be={}))[Ue.FINAL=0]="FINAL",Ue[Ue.ANY=1]="ANY",($e=Re||(Re={}))[$e.NORMAL=0]="NORMAL",$e[$e.ANY=1]="ANY",$e[$e.FLAT=2]="FLAT",(Le=Ge||(Ge={}))[Le.NORMAL=0]="NORMAL",Le[Le.REAL=1]="REAL";var Fe=e({name:"new-game-dialog",components:{ResponsiveImage:se},props:{image:{type:Object,required:!0}},emits:{newGame:null,bgclick:null},data:()=>({tiles:1e3,scoreMode:Be.ANY,shapeMode:Re.NORMAL,snapMode:Ge.NORMAL}),methods:{onNewGameClick(){this.$emit("newGame",{tiles:this.tilesInt,image:this.image,scoreMode:this.scoreModeInt,shapeMode:this.shapeModeInt,snapMode:this.snapModeInt})}},computed:{canStartNewGame(){return!!(this.tilesInt&&this.image&&this.image.url&&[0,1].includes(this.scoreModeInt))},scoreModeInt(){return parseInt(`${this.scoreMode}`,10)},shapeModeInt(){return parseInt(`${this.shapeMode}`,10)},snapModeInt(){return parseInt(`${this.snapMode}`,10)},tilesInt(){return parseInt(`${this.tiles}`,10)}}});const je={class:"area-image"},We={class:"has-image"},He={key:0,class:"image-title"},Ke={key:0,class:"image-title-title"},qe={key:1,class:"image-title-dim"},Ye={class:"area-settings"},Qe=n("td",null,[n("label",null,"Pieces")],-1),Ze=n("td",null,[n("label",null,"Scoring: ")],-1),Xe=i(" Any (Score when pieces are connected to each other or on final location)"),Je=n("br",null,null,-1),et=i(" Final (Score when pieces are put to their final location)"),tt=n("td",null,[n("label",null,"Shapes: ")],-1),nt=i(" Normal"),ot=n("br",null,null,-1),lt=i(" Any (flat pieces can occur anywhere)"),at=n("br",null,null,-1),st=i(" Flat (all pieces flat on all sides)"),it=n("td",null,[n("label",null,"Snapping: ")],-1),rt=i(" Normal (pieces snap to final destination and to each other)"),dt=n("br",null,null,-1),ct=i(" Real (pieces snap only to corners, already snapped pieces and to each other)"),ut={class:"area-buttons"};Fe.render=function(e,o,i,d,c,h){const m=a("responsive-image");return s(),t("div",{class:"overlay new-game-dialog",onClick:o[11]||(o[11]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[10]||(o[10]=u((()=>{}),["stop"]))},[n("div",je,[n("div",We,[n(m,{src:e.image.url,title:e.image.title},null,8,["src","title"])]),e.image.title||e.image.width||e.image.height?(s(),t("div",He,[e.image.title?(s(),t("span",Ke,'"'+r(e.image.title)+'"',1)):l("",!0),e.image.width||e.image.height?(s(),t("span",qe,"("+r(e.image.width)+" βœ• "+r(e.image.height)+")",1)):l("",!0)])):l("",!0)]),n("div",Ye,[n("table",null,[n("tr",null,[Qe,n("td",null,[p(n("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.tiles=t)},null,512),[[g,e.tiles]])])]),n("tr",null,[Ze,n("td",null,[n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[2]||(o[2]=t=>e.scoreMode=t),value:"1"},null,512),[[v,e.scoreMode]]),Xe]),Je,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[3]||(o[3]=t=>e.scoreMode=t),value:"0"},null,512),[[v,e.scoreMode]]),et])])]),n("tr",null,[tt,n("td",null,[n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[4]||(o[4]=t=>e.shapeMode=t),value:"0"},null,512),[[v,e.shapeMode]]),nt]),ot,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[5]||(o[5]=t=>e.shapeMode=t),value:"1"},null,512),[[v,e.shapeMode]]),lt]),at,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[6]||(o[6]=t=>e.shapeMode=t),value:"2"},null,512),[[v,e.shapeMode]]),st])])]),n("tr",null,[it,n("td",null,[n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[7]||(o[7]=t=>e.snapMode=t),value:"0"},null,512),[[v,e.snapMode]]),rt]),dt,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[8]||(o[8]=t=>e.snapMode=t),value:"1"},null,512),[[v,e.snapMode]]),ct])])])])]),n("div",ut,[n("button",{class:"btn",disabled:!e.canStartNewGame,onClick:o[9]||(o[9]=(...t)=>e.onNewGameClick&&e.onNewGameClick(...t))}," 🧩 Generate Puzzle ",8,["disabled"])])])])};var pt=e({components:{ImageLibrary:te,NewImageDialog:pe,EditImageDialog:ze,NewGameDialog:Fe},data:()=>({filters:{sort:"date_desc",tags:[]},images:[],tags:[],image:{id:0,filename:"",file:"",url:"",title:"",tags:[],created:0},dialog:"",uploading:"",uploadProgress:0}),async created(){await this.loadImages()},computed:{relevantTags(){return this.tags.filter((e=>e.total>0))}},methods:{autocompleteTags(e,t){return this.tags.filter((n=>!t.includes(n.title)&&n.title.toLowerCase().startsWith(e.toLowerCase()))).slice(0,10).map((e=>e.title))},toggleTag(e){this.filters.tags.includes(e.slug)?this.filters.tags=this.filters.tags.filter((t=>t!==e.slug)):this.filters.tags.push(e.slug),this.filtersChanged()},async loadImages(){const e=await _(`/api/newgame-data${ae.asQueryArgs(this.filters)}`,{}),t=await e.json();this.images=t.images,this.tags=t.tags},async filtersChanged(){await this.loadImages()},onImageClicked(e){this.image=e,this.dialog="new-game"},onImageEditClicked(e){this.image=e,this.dialog="edit-image"},async uploadImage(e){this.uploadProgress=0;const t=new FormData;t.append("file",e.file,e.file.name),t.append("title",e.title),t.append("tags",e.tags);const n=await V("/api/upload",{body:t,onUploadProgress:e=>{this.uploadProgress=e.loaded/e.total}});return this.uploadProgress=1,await n.json()},async saveImage(e){const t=await V("/api/save-image",{headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify({id:e.id,title:e.title,tags:e.tags})});return await t.json()},async onSaveImageClick(e){const t=await this.saveImage(e);t.ok?(this.dialog="",await this.loadImages()):alert(t.error)},async postToGalleryClick(e){this.uploading="postToGallery",await this.uploadImage(e),this.uploading="",this.dialog="",await this.loadImages()},async setupGameClick(e){this.uploading="setupGame";const t=await this.uploadImage(e);this.uploading="",this.loadImages(),this.image=t,this.dialog="new-game"},async onNewGame(e){const t=await V("/api/newgame",{headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(e)});if(200===t.status){const e=await t.json();this.$router.push({name:"game",params:{id:e.id}})}}}});const gt={class:"upload-image-teaser"},ht=n("div",{class:"hint"},"(The image you upload will be added to the public gallery.)",-1),mt={key:0},yt=i(" Tags: "),ft=i(" Sort by: "),vt=n("option",{value:"date_desc"},"Newest first",-1),wt=n("option",{value:"date_asc"},"Oldest first",-1),bt=n("option",{value:"alpha_asc"},"A-Z",-1),Ct=n("option",{value:"alpha_desc"},"Z-A",-1);pt.render=function(e,o,i,u,g,h){const m=a("image-library"),y=a("new-image-dialog"),f=a("edit-image-dialog"),v=a("new-game-dialog");return s(),t("div",null,[n("div",gt,[n("div",{class:"btn btn-big",onClick:o[1]||(o[1]=t=>e.dialog="new-image")},"Upload your image"),ht]),n("div",null,[e.tags.length>0?(s(),t("label",mt,[yt,(s(!0),t(d,null,c(e.relevantTags,((n,o)=>(s(),t("span",{class:["bit",{on:e.filters.tags.includes(n.slug)}],key:o,onClick:t=>e.toggleTag(n)},r(n.title)+" ("+r(n.total)+")",11,["onClick"])))),128))])):l("",!0),n("label",null,[ft,p(n("select",{"onUpdate:modelValue":o[2]||(o[2]=t=>e.filters.sort=t),onChange:o[3]||(o[3]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[vt,wt,bt,Ct],544),[[w,e.filters.sort]])])]),n(m,{images:e.images,onImageClicked:e.onImageClicked,onImageEditClicked:e.onImageEditClicked},null,8,["images","onImageClicked","onImageEditClicked"]),"new-image"===e.dialog?(s(),t(y,{key:0,autocompleteTags:e.autocompleteTags,onBgclick:o[4]||(o[4]=t=>e.dialog=""),uploadProgress:e.uploadProgress,uploading:e.uploading,onPostToGalleryClick:e.postToGalleryClick,onSetupGameClick:e.setupGameClick},null,8,["autocompleteTags","uploadProgress","uploading","onPostToGalleryClick","onSetupGameClick"])):l("",!0),"edit-image"===e.dialog?(s(),t(f,{key:1,autocompleteTags:e.autocompleteTags,onBgclick:o[5]||(o[5]=t=>e.dialog=""),onSaveClick:e.onSaveImageClick,image:e.image},null,8,["autocompleteTags","onSaveClick","image"])):l("",!0),e.image&&"new-game"===e.dialog?(s(),t(v,{key:2,onBgclick:o[6]||(o[6]=t=>e.dialog=""),onNewGame:e.onNewGame,image:e.image},null,8,["onNewGame","image"])):l("",!0)])};var xt=e({name:"scores",props:{activePlayers:{type:Array,required:!0},idlePlayers:{type:Array,required:!0}},computed:{actives(){return this.activePlayers.sort(((e,t)=>t.points-e.points)),this.activePlayers},idles(){return this.idlePlayers.sort(((e,t)=>t.points-e.points)),this.idlePlayers}}});const kt={class:"scores"},Pt=n("div",null,"Scores",-1),At=n("td",null,"⚑",-1),St=n("td",null,"πŸ’€",-1);xt.render=function(e,o,l,a,i,u){return s(),t("div",kt,[Pt,n("table",null,[(s(!0),t(d,null,c(e.actives,((e,o)=>(s(),t("tr",{key:o,style:{color:e.color}},[At,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128)),(s(!0),t(d,null,c(e.idles,((e,o)=>(s(),t("tr",{key:o,style:{color:e.color}},[St,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128))])])};var zt=e({name:"puzzle-status",props:{finished:{type:Boolean,required:!0},duration:{type:Number,required:!0},piecesDone:{type:Number,required:!0},piecesTotal:{type:Number,required:!0}},computed:{icon(){return this.finished?"🏁":"⏳"},durationStr(){return j(this.duration)}}});const Tt={class:"timer"};zt.render=function(e,o,l,a,i,d){return s(),t("div",Tt,[n("div",null," 🧩 "+r(e.piecesDone)+"/"+r(e.piecesTotal),1),n("div",null,r(e.icon)+" "+r(e.durationStr),1),b(e.$slots,"default")])};var It=e({name:"settings-overlay",emits:{bgclick:null,"update:modelValue":null},props:{modelValue:{type:Object,required:!0}},methods:{updateVolume(e){this.modelValue.soundsVolume=e.target.value},decreaseVolume(){const e=parseInt(this.modelValue.soundsVolume,10)-5;this.modelValue.soundsVolume=Math.max(0,e)},increaseVolume(){const e=parseInt(this.modelValue.soundsVolume,10)+5;this.modelValue.soundsVolume=Math.min(100,e)}},created(){this.$watch("modelValue",(e=>{this.$emit("update:modelValue",e)}),{deep:!0})}});const Et=m();y("data-v-4d56fc17");const Mt=n("td",null,[n("label",null,"Background: ")],-1),Dt=n("td",null,[n("label",null,"Color: ")],-1),Nt=n("td",null,[n("label",null,"Name: ")],-1),_t=n("td",null,[n("label",null,"Sounds: ")],-1),Vt=n("td",null,[n("label",null,"Sounds Volume: ")],-1),Ot={class:"sound-volume"},Bt=n("td",null,[n("label",null,"Show player names: ")],-1);f();const Ut=Et(((e,o,l,a,i,r)=>(s(),t("div",{class:"overlay transparent",onClick:o[10]||(o[10]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content settings",onClick:o[9]||(o[9]=u((()=>{}),["stop"]))},[n("tr",null,[Mt,n("td",null,[p(n("input",{type:"color","onUpdate:modelValue":o[1]||(o[1]=t=>e.modelValue.background=t)},null,512),[[g,e.modelValue.background]])])]),n("tr",null,[Dt,n("td",null,[p(n("input",{type:"color","onUpdate:modelValue":o[2]||(o[2]=t=>e.modelValue.color=t)},null,512),[[g,e.modelValue.color]])])]),n("tr",null,[Nt,n("td",null,[p(n("input",{type:"text",maxLength:"16","onUpdate:modelValue":o[3]||(o[3]=t=>e.modelValue.name=t)},null,512),[[g,e.modelValue.name]])])]),n("tr",null,[_t,n("td",null,[p(n("input",{type:"checkbox","onUpdate:modelValue":o[4]||(o[4]=t=>e.modelValue.soundsEnabled=t)},null,512),[[C,e.modelValue.soundsEnabled]])])]),n("tr",null,[Vt,n("td",Ot,[n("span",{onClick:o[5]||(o[5]=(...t)=>e.decreaseVolume&&e.decreaseVolume(...t))},"πŸ”‰"),n("input",{type:"range",min:"0",max:"100",value:e.modelValue.soundsVolume,onChange:o[6]||(o[6]=(...t)=>e.updateVolume&&e.updateVolume(...t))},null,40,["value"]),n("span",{onClick:o[7]||(o[7]=(...t)=>e.increaseVolume&&e.increaseVolume(...t))},"πŸ”Š")])]),n("tr",null,[Bt,n("td",null,[p(n("input",{type:"checkbox","onUpdate:modelValue":o[8]||(o[8]=t=>e.modelValue.showPlayerNames=t)},null,512),[[C,e.modelValue.showPlayerNames]])])])])]))));It.render=Ut,It.__scopeId="data-v-4d56fc17";var Rt=e({name:"preview-overlay",props:{img:String},emits:{bgclick:null},computed:{previewStyle(){return{backgroundImage:`url('${this.img}')`}}}});const $t={class:"preview"};Rt.render=function(e,o,l,a,i,r){return s(),t("div",{class:"overlay",onClick:o[1]||(o[1]=t=>e.$emit("bgclick"))},[n("div",$t,[n("div",{class:"img",style:e.previewStyle},null,4)])])};var Gt=e({name:"help-overlay",emits:{bgclick:null},props:{game:{type:Object,required:!0}},computed:{scoreMode(){switch(this.game.scoreMode){case Be.ANY:return["Any","Score when pieces are connected to each other or on final location"];case Be.FINAL:default:return["Final","Score when pieces are put to their final location"]}},shapeMode(){switch(this.game.shapeMode){case Re.FLAT:return["Flat","All pieces flat on all sides"];case Re.ANY:return["Any","Flat pieces can occur anywhere"];case Re.NORMAL:default:return["Normal",""]}},snapMode(){switch(this.game.snapMode){case Ge.REAL:return["Real","Pieces snap only to corners, already snapped pieces and to each other"];case Ge.NORMAL:default:return["Normal","Pieces snap to final destination and to each other"]}}}});const Lt=n("tr",null,[n("td",{colspan:"2"},"Info about this puzzle")],-1),Ft=n("td",null,"Image Title: ",-1),jt=n("td",null,"Scoring: ",-1),Wt=n("td",null,"Shapes: ",-1),Ht=n("td",null,"Snapping: ",-1);Gt.render=function(e,o,l,a,i,d){return s(),t("div",{class:"overlay transparent",onClick:o[2]||(o[2]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content help",onClick:o[1]||(o[1]=u((()=>{}),["stop"]))},[Lt,n("tr",null,[Ft,n("td",null,r(e.game.puzzle.info.image.title),1)]),n("tr",null,[jt,n("td",null,[n("span",{title:e.snapMode[1]},r(e.scoreMode[0]),9,["title"])])]),n("tr",null,[Wt,n("td",null,[n("span",{title:e.snapMode[1]},r(e.shapeMode[0]),9,["title"])])]),n("tr",null,[Ht,n("td",null,[n("span",{title:e.snapMode[1]},r(e.snapMode[0]),9,["title"])])])])])};var Kt=1,qt=4,Yt=2,Qt=3,Zt=2,Xt=4,Jt=3,en=9,tn=1,nn=2,on=3,ln=4,an=5,sn=6,rn=7,dn=8,cn=10,un=11,pn=12,gn=13,hn=14,mn=15,yn=16,fn=17,vn=18,wn=1,bn=2,Cn=3;const xn=le("Communication.js");let kn,Pn=[],An=e=>{Pn.push(e)},Sn=[],zn=e=>{Sn.push(e)};let Tn=0;const In=e=>{Tn!==e&&(Tn=e,zn(e))};function En(e){if(2===Tn)try{kn.send(JSON.stringify(e))}catch(t){xn.info("unable to send message.. maybe because ws is invalid?")}}let Mn,Dn;var Nn={connect:function(e,t,n){return Mn=0,Dn={},In(3),new Promise((o=>{kn=new WebSocket(e,n+"|"+t),kn.onopen=()=>{In(2),En([Qt])},kn.onmessage=e=>{const t=JSON.parse(e.data),l=t[0];if(l===qt){const e=t[1];o(e)}else{if(l!==Kt)throw`[ 2021-05-09 invalid connect msgType ${l} ]`;{const e=t[1],o=t[2];if(e===n&&Dn[o])return void delete Dn[o];An(t)}}},kn.onerror=()=>{throw In(1),"[ 2021-05-15 onerror ]"},kn.onclose=e=>{4e3===e.code||1001===e.code?In(4):In(1)}}))},requestReplayData:async function(e,t){const n={gameId:e,offset:t},o=await _(`/api/replay-data${ae.asQueryArgs(n)}`,{});return await o.json()},disconnect:function(){kn&&kn.close(4e3),Mn=0,Dn={}},sendClientEvent:function(e){Mn++,Dn[Mn]=e,En([Yt,Mn,Dn[Mn]])},onServerChange:function(e){An=e;for(const t of Pn)An(t);Pn=[]},onConnectionStateChange:function(e){zn=e;for(const t of Sn)zn(t);Sn=[]},CODE_CUSTOM_DISCONNECT:4e3,CONN_STATE_NOT_CONNECTED:0,CONN_STATE_DISCONNECTED:1,CONN_STATE_CLOSED:4,CONN_STATE_CONNECTED:2,CONN_STATE_CONNECTING:3},_n=e({name:"connection-overlay",emits:{reconnect:null},props:{connectionState:Number},computed:{lostConnection(){return this.connectionState===Nn.CONN_STATE_DISCONNECTED},connecting(){return this.connectionState===Nn.CONN_STATE_CONNECTING},show(){return!(!this.lostConnection&&!this.connecting)}}});const Vn={key:0,class:"overlay connection-lost"},On={key:0,class:"overlay-content"},Bn=n("div",null,"⁉️ LOST CONNECTION ⁉️",-1),Un={key:1,class:"overlay-content"},Rn=n("div",null,"Connecting...",-1);_n.render=function(e,o,a,i,r,d){return e.show?(s(),t("div",Vn,[e.lostConnection?(s(),t("div",On,[Bn,n("span",{class:"btn",onClick:o[1]||(o[1]=t=>e.$emit("reconnect"))},"Reconnect")])):l("",!0),e.connecting?(s(),t("div",Un,[Rn])):l("",!0)])):l("",!0)};var $n=e({name:"help-overlay",emits:{bgclick:null}});const Gn=n("tr",null,[n("td",null,"⬆️ Move up:"),n("td",null,[n("div",null,[n("kbd",null,"W"),i("/"),n("kbd",null,"↑"),i("/πŸ–±οΈ")])])],-1),Ln=n("tr",null,[n("td",null,"⬇️ Move down:"),n("td",null,[n("div",null,[n("kbd",null,"S"),i("/"),n("kbd",null,"↓"),i("/πŸ–±οΈ")])])],-1),Fn=n("tr",null,[n("td",null,"⬅️ Move left:"),n("td",null,[n("div",null,[n("kbd",null,"A"),i("/"),n("kbd",null,"←"),i("/πŸ–±οΈ")])])],-1),jn=n("tr",null,[n("td",null,"➑️ Move right:"),n("td",null,[n("div",null,[n("kbd",null,"D"),i("/"),n("kbd",null,"β†’"),i("/πŸ–±οΈ")])])],-1),Wn=n("tr",null,[n("td"),n("td",null,[n("div",null,[i("Move faster by holding "),n("kbd",null,"Shift")])])],-1),Hn=n("tr",null,[n("td",null,"πŸ”+ Zoom in:"),n("td",null,[n("div",null,[n("kbd",null,"E"),i("/πŸ–±οΈ-Wheel")])])],-1),Kn=n("tr",null,[n("td",null,"πŸ”- Zoom out:"),n("td",null,[n("div",null,[n("kbd",null,"Q"),i("/πŸ–±οΈ-Wheel")])])],-1),qn=n("tr",null,[n("td",null,"πŸ–ΌοΈ Toggle preview:"),n("td",null,[n("div",null,[n("kbd",null,"Space")])])],-1),Yn=n("tr",null,[n("td",null,"🎯 Center puzzle in screen:"),n("td",null,[n("div",null,[n("kbd",null,"C")])])],-1),Qn=n("tr",null,[n("td",null,"πŸ§©βœ”οΈ Toggle fixed pieces:"),n("td",null,[n("div",null,[n("kbd",null,"F")])])],-1),Zn=n("tr",null,[n("td",null,"πŸ§©β“ Toggle loose pieces:"),n("td",null,[n("div",null,[n("kbd",null,"G")])])],-1),Xn=n("tr",null,[n("td",null,"πŸ‘€ Toggle player names:"),n("td",null,[n("div",null,[n("kbd",null,"N")])])],-1),Jn=n("tr",null,[n("td",null,"πŸ”‰ Toggle sounds:"),n("td",null,[n("div",null,[n("kbd",null,"M")])])],-1),eo=n("tr",null,[n("td",null,"⏫ Speed up (replay):"),n("td",null,[n("div",null,[n("kbd",null,"I")])])],-1),to=n("tr",null,[n("td",null,"⏬ Speed down (replay):"),n("td",null,[n("div",null,[n("kbd",null,"O")])])],-1),no=n("tr",null,[n("td",null,"⏸️ Pause (replay):"),n("td",null,[n("div",null,[n("kbd",null,"P")])])],-1);$n.render=function(e,o,l,a,i,r){return s(),t("div",{class:"overlay transparent",onClick:o[2]||(o[2]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content help",onClick:o[1]||(o[1]=u((()=>{}),["stop"]))},[Gn,Ln,Fn,jn,Wn,Hn,Kn,qn,Yn,Qn,Zn,Xn,Jn,eo,to,no])])};var oo=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"/assets/click.bb97cb07.mp3"}),lo=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAW0lEQVQ4je1RywrAIAxLxP//5exixRWlVgZelpOKeTQFfnDypgy3eLIkSLLL8mxGPoHsU2hPAgDHBLvRX6hZZw/fwT0BtlLSONqCbWAmEIqMZOCDDlaDR3N03gOyDCn+y4DWmAAAAABJRU5ErkJggg=="}),ao=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARElEQVQ4jWNgGAU0Af+hmBCbgYGBgYERhwHEAEYGBgYGJtIdiApYyLAZBVDsAqoagC1ACQJyY4ERg0GCISh6KA4DigEAou8LC+LnIJoAAAAASUVORK5CYII="}),so=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVQ4ja1TQQ7AIAgD///n7jCozA2Hbk00jbG1KIrcARszTugoBs49qioZj7r2kKACptkyAOCJsJuA+GzglwHjvMSSWFVaENWVASxh5eRLiq5fN/ASjI89VcP2K3hHpq1cEXNaMfnrL3TDZP2tDuoOA6MzCCXWr38AAAAASUVORK5CYII="}),io=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAU0lEQVQ4jWNgoAH4D8X42HDARKlt5BoAd82AuQAOGLGIYQQUPv0wF5CiCQUge4EsQ5C9QI4BjMguwBYeBAElscCIy1ZivMKIwSDBEBQ9FCckigEAU3QOD7TGvY4AAAAASUVORK5CYII="});function ro(e=0,t=0){const n=document.createElement("canvas");return n.width=e,n.height=t,n}var co={createCanvas:ro,loadImageToBitmap:async function(e){return new Promise((t=>{const n=new Image;n.onload=()=>{createImageBitmap(n).then(t)},n.src=e}))},resizeBitmap:async function(e,t,n){const o=ro(t,n);return o.getContext("2d").drawImage(e,0,0,e.width,e.height,0,0,t,n),await createImageBitmap(o)},colorizedCanvas:function(e,t,n){const o=ro(e.width,e.height),l=o.getContext("2d");return l.save(),l.drawImage(t,0,0),l.fillStyle=n,l.globalCompositeOperation="source-in",l.fillRect(0,0,t.width,t.height),l.restore(),l.save(),l.globalCompositeOperation="destination-over",l.drawImage(e,0,0),l.restore(),o}};const uo=le("Debug.js");let po=0,go=0;var ho=e=>{po=performance.now(),go=e},mo=e=>{const t=performance.now(),n=t-po;n>go&&uo.log(e+": "+n),po=t};function yo(e,t){const n=e.x-t.x,o=e.y-t.y;return Math.sqrt(n*n+o*o)}function fo(e){return{x:e.x+e.w/2,y:e.y+e.h/2}}var vo={pointSub:function(e,t){return{x:e.x-t.x,y:e.y-t.y}},pointAdd:function(e,t){return{x:e.x+t.x,y:e.y+t.y}},pointDistance:yo,pointInBounds:function(e,t){return e.x>=t.x&&e.x<=t.x+t.w&&e.y>=t.y&&e.y<=t.y+t.h},rectCenter:fo,rectMoved:function(e,t,n){return{x:e.x+t,y:e.y+n,w:e.w,h:e.h}},rectCenterDistance:function(e,t){return yo(fo(e),fo(t))},rectsOverlap:function(e,t){return!(t.x>e.x+e.w||e.x>t.x+t.w||t.y>e.y+e.h||e.y>t.y+t.h)}};const wo=le("PuzzleGraphics.js");function bo(e,t){const n=ae.coordByPieceIdx(e,t);return{x:n.x*e.tileSize,y:n.y*e.tileSize,w:e.tileSize,h:e.tileSize}}var Co={loadPuzzleBitmaps:async function(e){const t=await co.loadImageToBitmap(e.info.imageUrl),n=await co.resizeBitmap(t,e.info.width,e.info.height);return await async function(e,t,n){wo.log("start createPuzzleTileBitmaps");const o=n.tileSize,l=n.tileMarginWidth,a=n.tileDrawSize,s=o/100,i=[0,0,40,15,37,5,37,5,40,0,38,-5,38,-5,20,-20,50,-20,50,-20,80,-20,62,-5,62,-5,60,0,63,5,63,5,65,15,100,0],r=new Array(t.length),d={};function c(e){const t=`${e.top}${e.right}${e.left}${e.bottom}`;if(d[t])return d[t];const n=new Path2D,a={x:l,y:l},r=vo.pointAdd(a,{x:o,y:0}),c=vo.pointAdd(r,{x:0,y:o}),u=vo.pointSub(c,{x:o,y:0});if(n.moveTo(a.x,a.y),0!==e.top)for(let o=0;oae.decodePiece(xo[e].puzzle.tiles[t]),Bo=(e,t)=>Oo(e,t).group,Uo=(e,t)=>{const n=xo[e].puzzle.info;return 0===t||t===n.tilesX-1||t===n.tiles-n.tilesX||t===n.tiles-1},Ro=(e,t)=>{const n=xo[e].puzzle.info,o={x:(n.table.width-n.width)/2,y:(n.table.height-n.height)/2},l=function(e,t){const n=xo[e].puzzle.info,o=ae.coordByPieceIdx(n,t),l=o.x*n.tileSize,a=o.y*n.tileSize;return{x:l,y:a}}(e,t);return vo.pointAdd(o,l)},$o=(e,t)=>Oo(e,t).pos,Go=e=>{const t=ll(e),n=al(e),o=Math.round(t/4),l=Math.round(n/4);return{x:0-o,y:0-l,w:t+2*o,h:n+2*l}},Lo=(e,t)=>{const n=Ho(e),o=Oo(e,t);return{x:o.pos.x,y:o.pos.y,w:n,h:n}},Fo=(e,t)=>Oo(e,t).z,jo=(e,t)=>{for(const n of xo[e].puzzle.tiles){const e=ae.decodePiece(n);if(e.owner===t)return e.idx}return-1},Wo=e=>xo[e].puzzle.info.tileDrawSize,Ho=e=>xo[e].puzzle.info.tileSize,Ko=e=>xo[e].puzzle.data.maxGroup,qo=e=>xo[e].puzzle.data.maxZ;function Yo(e,t){const n=xo[e].puzzle.info,o=ae.coordByPieceIdx(n,t);return[o.y>0?t-n.tilesX:-1,o.x0?t-1:-1]}const Qo=(e,t,n)=>{for(const o of t)Vo(e,o,{z:n})},Zo=(e,t,n)=>{const o=$o(e,t);Vo(e,t,{pos:vo.pointAdd(o,n)})},Xo=(e,t,n)=>{const o=Wo(e),l=Go(e),a=n;for(const s of t){const t=Oo(e,s);t.pos.x+n.xl.x+l.w&&(a.x=Math.min(l.x+l.w-t.pos.x+o,a.x)),t.pos.y+n.yl.y+l.h&&(a.y=Math.min(l.y+l.h-t.pos.y+o,a.y))}for(const s of t)Zo(e,s,a)},Jo=(e,t)=>Oo(e,t).owner,el=(e,t)=>{for(const n of t)Vo(e,n,{owner:-1,z:1})},tl=(e,t,n)=>{for(const o of t)Vo(e,o,{owner:n})};function nl(e,t){const n=xo[e].puzzle.tiles,o=ae.decodePiece(n[t]),l=[];if(o.group)for(const a of n){const e=ae.decodePiece(a);e.group===o.group&&l.push(e.idx)}else l.push(o.idx);return l}const ol=(e,t)=>{const n=Po(e,t);return n?n.points:0},ll=e=>xo[e].puzzle.info.table.width,al=e=>xo[e].puzzle.info.table.height;var sl={setGame:function(e,t){xo[e]=t},exists:function(e){return!!xo[e]||!1},playerExists:So,getActivePlayers:function(e,t){const n=t-30*G;return zo(e).filter((e=>e.ts>=n))},getIdlePlayers:function(e,t){const n=t-30*G;return zo(e).filter((e=>e.ts0))},addPlayer:function(e,t,n){So(e,t)?No(e,t,{ts:n}):Ao(e,t,function(e,t){return{id:e,x:0,y:0,d:0,name:null,color:null,bgcolor:null,points:0,ts:t}}(t,n))},getFinishedPiecesCount:Do,getPieceCount:To,getImageUrl:function(e){var t;const n=(null==(t=xo[e].puzzle.info.image)?void 0:t.url)||xo[e].puzzle.info.imageUrl;if(!n)throw new Error("[2021-07-11] no image url set");return n},get:function(e){return xo[e]||null},getAllGames:function(){return Object.values(xo).sort(((e,t)=>{const n=Mo(e.id);return n===Mo(t.id)?n?t.puzzle.data.finished-e.puzzle.data.finished:t.puzzle.data.started-e.puzzle.data.started:n?1:-1}))},getPlayerBgColor:(e,t)=>{const n=Po(e,t);return n?n.bgcolor:null},getPlayerColor:(e,t)=>{const n=Po(e,t);return n?n.color:null},getPlayerName:(e,t)=>{const n=Po(e,t);return n?n.name:null},getPlayerIndexById:ko,getPlayerIdByIndex:function(e,t){return xo[e].players.length>t?ae.decodePlayer(xo[e].players[t]).id:null},changePlayer:No,setPlayer:Ao,setPiece:function(e,t,n){xo[e].puzzle.tiles[t]=ae.encodePiece(n)},setPuzzleData:function(e,t){xo[e].puzzle.data=t},getTableWidth:ll,getTableHeight:al,getPuzzle:e=>xo[e].puzzle,getRng:e=>xo[e].rng.obj,getPuzzleWidth:e=>xo[e].puzzle.info.width,getPuzzleHeight:e=>xo[e].puzzle.info.height,getPiecesSortedByZIndex:function(e){return xo[e].puzzle.tiles.map(ae.decodePiece).sort(((e,t)=>e.z-t.z))},getFirstOwnedPiece:(e,t)=>{const n=jo(e,t);return n<0?null:xo[e].puzzle.tiles[n]},getPieceDrawOffset:e=>xo[e].puzzle.info.tileDrawOffset,getPieceDrawSize:Wo,getFinalPiecePos:Ro,getStartTs:e=>xo[e].puzzle.data.started,getFinishTs:e=>xo[e].puzzle.data.finished,handleInput:function(e,t,n,o,l){const a=xo[e].puzzle,s=function(e,t){return t in xo[e].evtInfos?xo[e].evtInfos[t]:{_last_mouse:null,_last_mouse_down:null}}(e,t),i=[],r=()=>{i.push([wn,a.data])},d=t=>{i.push([bn,ae.encodePiece(Oo(e,t))])},c=e=>{for(const t of e)d(t)},u=()=>{const n=Po(e,t);n&&i.push([Cn,ae.encodePlayer(n)])},p=n[0];if(p===sn){const l=n[1];No(e,t,{bgcolor:l,ts:o}),u()}else if(p===rn){const l=n[1];No(e,t,{color:l,ts:o}),u()}else if(p===dn){const l=`${n[1]}`.substr(0,16);No(e,t,{name:l,ts:o}),u()}else if(p===en){const l=n[1],a=n[2],s=Po(e,t);if(s){const n=s.x-l,i=s.y-a;No(e,t,{ts:o,x:n,y:i}),u()}}else if(p===tn){const l={x:n[1],y:n[2]};No(e,t,{d:1,ts:o}),u(),s._last_mouse_down=l;const a=((e,t)=>{const n=xo[e].puzzle.info,o=xo[e].puzzle.tiles;let l=-1,a=-1;for(let s=0;sl)&&(l=e.z,a=s)}return a})(e,l);if(a>=0){const n=qo(e)+1;_o(e,{maxZ:n}),r();const o=nl(e,a);Qo(e,o,qo(e)),tl(e,o,t),c(o)}s._last_mouse=l}else if(p===on){const l=n[1],a=n[2],i={x:l,y:a};if(null===s._last_mouse_down)No(e,t,{x:l,y:a,ts:o}),u();else{const n=jo(e,t);if(n>=0){No(e,t,{x:l,y:a,ts:o}),u();const r=nl(e,n);let d=vo.pointInBounds(i,Go(e))&&vo.pointInBounds(s._last_mouse_down,Go(e));for(const t of r){const n=Lo(e,t);if(vo.pointInBounds(i,n)){d=!0;break}}if(d){const t=l-s._last_mouse_down.x,n=a-s._last_mouse_down.y;Xo(e,r,{x:t,y:n}),c(r)}}else No(e,t,{ts:o}),u();s._last_mouse_down=i}s._last_mouse=i}else if(p===nn){const i={x:n[1],y:n[2]},p=0;s._last_mouse_down=null;const g=jo(e,t);if(g>=0){const n=nl(e,g);tl(e,n,0),c(n);const s=$o(e,g),i=Ro(e,g);let h=!1;if(Eo(e)===Ge.REAL){for(const t of n)if(Uo(e,t)){h=!0;break}}else h=!0;if(h&&vo.pointDistance(i,s){const l=xo[e].puzzle.info;if(n<0)return!1;if(((e,t,n)=>{const o=Bo(e,t),l=Bo(e,n);return!(!o||o!==l)})(e,t,n))return!1;const a=$o(e,t),s=vo.pointAdd($o(e,n),{x:o[0]*l.tileSize,y:o[1]*l.tileSize});if(vo.pointDistance(a,s){const o=xo[e].puzzle.tiles,l=Bo(e,t),a=Bo(e,n);let s;const i=[];l&&i.push(l),a&&i.push(a),l?s=l:a?s=a:(_o(e,{maxGroup:Ko(e)+1}),r(),s=Ko(e));if(Vo(e,t,{group:s}),d(t),Vo(e,n,{group:s}),d(n),i.length>0)for(const r of o){const t=ae.decodePiece(r);i.includes(t.group)&&(Vo(e,t.idx,{group:s}),d(t.idx))}})(e,t,n),l=nl(e,t),((e,t)=>-1===Jo(e,t))(e,n))el(e,l);else{const t=((e,t)=>{let n=0;for(const o of t){const t=Fo(e,o);t>n&&(n=t)}return n})(e,l);Qo(e,l,t)}return c(l),!0}return!1};let a=!1;for(const t of nl(e,g)){const o=Yo(e,t);if(n(e,t,o[0],[0,1])||n(e,t,o[1],[-1,0])||n(e,t,o[2],[0,-1])||n(e,t,o[3],[1,0])){a=!0;break}}if(a&&Io(e)===Be.ANY){const n=ol(e,t)+1;No(e,t,{d:p,ts:o,points:n}),u()}else No(e,t,{d:p,ts:o}),u();a&&Eo(e)===Ge.REAL&&Do(e)===To(e)&&(_o(e,{finished:o}),r()),a&&l&&l(t)}}else No(e,t,{d:p,ts:o}),u();s._last_mouse=i}else if(p===ln){const l=n[1],a=n[2];No(e,t,{x:l,y:a,ts:o}),u(),s._last_mouse={x:l,y:a}}else if(p===an){const l=n[1],a=n[2];No(e,t,{x:l,y:a,ts:o}),u(),s._last_mouse={x:l,y:a}}else No(e,t,{ts:o}),u();return function(e,t,n){xo[e].evtInfos[t]=n}(e,t,s),i}};let il=-10,rl=20,dl=2,cl=15;class ul{constructor(e){this.radius=10,this.previousRadius=10,this.explodingDuration=100,this.hasExploded=!1,this.alive=!0,this.color=function(e){return"rgba("+e.random(0,255)+","+e.random(0,255)+","+e.random(0,255)+", 0.8)"}(e),this.px=window.innerWidth/4+Math.random()*window.innerWidth/2,this.py=window.innerHeight,this.vx=il+Math.random()*rl,this.vy=-1*(dl+Math.random()*cl),this.duration=0}update(e){if(this.hasExploded){const e=200-this.radius;this.previousRadius=this.radius,this.radius+=e/10,this.explodingDuration--,0==this.explodingDuration&&(this.alive=!1)}else this.vx+=0,this.vy+=1,this.vy>=0&&e&&this.explode(e),this.px+=this.vx,this.py+=this.vy}draw(e){e.beginPath(),e.arc(this.px,this.py,this.previousRadius,0,2*Math.PI,!1),this.hasExploded||(e.fillStyle=this.color,e.lineWidth=1,e.fill())}explode(e){this.hasExploded=!0;const t=3+Math.floor(3*Math.random());for(let n=0;n{this.resize()}))}setSpeedParams(){let e=0,t=0;for(;e=0;)t+=1,e+=t;dl=t/2,cl=t-dl;const n=1/4*this.canvas.width/(t/2);il=-n,rl=2*n}resize(){this.setSpeedParams()}init(){this.readyBombs=[],this.explodedBombs=[],this.particles=[];for(let e=0;e<1;e++)this.readyBombs.push(new ul(this.rng))}update(){100*Math.random()<5&&this.readyBombs.push(new ul(this.rng));const e=[];for(;this.explodedBombs.length>0;){const t=this.explodedBombs.shift();if(!t)break;t.update(),t.alive&&e.push(t)}this.explodedBombs=e;const t=[];for(;this.readyBombs.length>0;){const e=this.readyBombs.shift();if(!e)break;e.update(this.particles),e.hasExploded?this.explodedBombs.push(e):t.push(e)}this.readyBombs=t;const n=[];for(;this.particles.length>0;){const e=this.particles.shift();if(!e)break;e.update(),e.alive&&n.push(e)}this.particles=n}render(){this.ctx.beginPath(),this.ctx.fillStyle="rgba(0, 0, 0, 0.1)",this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);for(let e=0;e{localStorage.setItem(e,t)},Cl=e=>localStorage.getItem(e);var xl=(e,t)=>{bl(e,`${t}`)},kl=(e,t)=>{const n=Cl(e);if(null===n)return t;const o=parseInt(n,10);return isNaN(o)?t:o},Pl=(e,t)=>{bl(e,t?"1":"0")},Al=(e,t)=>{const n=Cl(e);return null===n?t:"1"===n},Sl=(e,t)=>{bl(e,t)},zl=(e,t)=>{const n=Cl(e);return null===n?t:n};const Tl={"./grab.png":lo,"./grab_mask.png":ao,"./hand.png":so,"./hand_mask.png":io},Il={"./click.mp3":oo},El="replay";let Ml=!0,Dl=!0;let Nl=!0;async function _l(e,t,n,o,l,a){void 0===window.DEBUG&&(window.DEBUG=!1);const s=Il["./click.mp3"].default,i=new Audio(s),r=await co.loadImageToBitmap(Tl["./grab.png"].default),d=await co.loadImageToBitmap(Tl["./hand.png"].default),c=await co.loadImageToBitmap(Tl["./grab_mask.png"].default),u=await co.loadImageToBitmap(Tl["./hand_mask.png"].default),p=r.width,g=Math.round(p/2),h=r.height,m=Math.round(h/2),y={},f=async e=>{const t=e.color+" "+e.d;if(!y[t]){const n=e.d?r:d;if(e.color){const o=e.d?c:u;y[t]=await createImageBitmap(co.colorizedCanvas(n,o,e.color))}else y[t]=n}return y[t]},v=function(e,t){return t.width=window.innerWidth,t.height=window.innerHeight,e.appendChild(t),window.addEventListener("resize",(()=>{t.width=window.innerWidth,t.height=window.innerHeight,Nl=!0})),t}(l,co.createCanvas()),w={final:!1,log:[],logPointer:0,speeds:[.5,1,2,5,10,20,50,100,250,500],speedIdx:1,paused:!1,lastRealTs:0,lastGameTs:0,gameStartTs:0,skipNonActionPhases:!0,dataOffset:0};Nn.onConnectionStateChange((e=>{a.setConnectionState(e)}));const b=async e=>{const t=w.dataOffset;w.dataOffset+=1e4;const n=await Nn.requestReplayData(e,t);return w.log=w.log.slice(w.logPointer),w.logPointer=0,w.log.push(...n.log),0===n.log.length&&(w.final=!0),n};let C=()=>0;const x=async()=>{if("play"===o){const o=await Nn.connect(n,e,t),l=ae.decodeGame(o);sl.setGame(l.id,l),C=()=>L()}else{if(o!==El)throw"[ 2020-12-22 MODE invalid, must be play|replay ]";{const t=await b(e);if(!t.game)throw"[ 2021-05-29 no game received ]";const n=ae.decodeGame(t.game);sl.setGame(n.id,n),w.lastRealTs=L(),w.gameStartTs=parseInt(t.log[0][4],10),w.lastGameTs=w.gameStartTs,C=()=>w.lastGameTs}}Nl=!0};await x();const k=sl.getPieceDrawOffset(e),P=sl.getPieceDrawSize(e),A=sl.getPuzzleWidth(e),S=sl.getPuzzleHeight(e),z=sl.getTableWidth(e),T=sl.getTableHeight(e),I={x:(z-A)/2,y:(T-S)/2},E={w:A,h:S},M={w:P,h:P},D=await Co.loadPuzzleBitmaps(sl.getPuzzle(e)),N=new gl(v,sl.getRng(e));N.init();const _=v.getContext("2d");v.classList.add("loaded"),a.setPuzzleCut();const V=function(){let e=0,t=0,n=1;const o=(o,l)=>{e+=o/n,t+=l/n},l=e=>{const t=n+.05*n*("in"===e?1:-1);return Math.min(Math.max(t,.1),6)},a=(e,t)=>{if(n==e)return!1;const l=1-n/e;return o(-t.x*l,-t.y*l),n=e,!0},s=o=>({x:o.x/n-e,y:o.y/n-t}),i=o=>({x:(o.x+e)*n,y:(o.y+t)*n}),r=e=>({w:e.w*n,h:e.h*n}),d=e=>({w:e.w/n,h:e.h/n});return{getCurrentZoom:()=>n,reset:()=>{e=0,t=0,n=1},move:o,canZoom:e=>n!=l(e),zoom:(e,t)=>a(l(e),t),setZoom:a,worldToViewport:e=>{const{x:t,y:n}=i(e);return{x:Math.round(t),y:Math.round(n)}},worldToViewportRaw:i,worldDimToViewport:e=>{const{w:t,h:n}=r(e);return{w:Math.round(t),h:Math.round(n)}},worldDimToViewportRaw:r,viewportToWorld:e=>{const{x:t,y:n}=s(e);return{x:Math.round(t),y:Math.round(n)}},viewportToWorldRaw:s,viewportDimToWorld:e=>{const{w:t,h:n}=d(e);return{w:Math.round(t),h:Math.round(n)}},viewportDimToWorldRaw:d}}(),O=()=>{V.reset(),V.move(-(z-v.width)/2,-(T-v.height)/2);const e=V.worldDimToViewport(E),t=v.width-40,n=v.height-40;if(e.w>t||e.h>n||e.w{const o=n.viewportToWorld({x:e,y:t});return[o.x,o.y]},h=e=>g(e.offsetX,e.offsetY),m=()=>g(e.width/2,e.height/2),y=(e,t)=>{a&&("ShiftLeft"===t.code||"ShiftRight"===t.code?p=e:"ArrowUp"===t.code||"KeyW"===t.code?r=e:"ArrowDown"===t.code||"KeyS"===t.code?d=e:"ArrowLeft"===t.code||"KeyA"===t.code?s=e:"ArrowRight"===t.code||"KeyD"===t.code?i=e:"KeyQ"===t.code?u=e:"KeyE"===t.code&&(c=e))};let f=null;e.addEventListener("mousedown",(e=>{f=h(e),0===e.button&&v([tn,...f])})),e.addEventListener("mouseup",(e=>{f=h(e),0===e.button&&v([nn,...f])})),e.addEventListener("mousemove",(e=>{f=h(e),v([on,...f])})),e.addEventListener("wheel",(e=>{if(f=h(e),n.canZoom(e.deltaY<0?"in":"out")){const t=e.deltaY<0?ln:an;v([t,...f])}})),t.addEventListener("keydown",(e=>y(!0,e))),t.addEventListener("keyup",(e=>y(!1,e))),t.addEventListener("keypress",(e=>{a&&("Space"===e.code&&v([cn]),o===El&&("KeyI"===e.code&&v([gn]),"KeyO"===e.code&&v([hn]),"KeyP"===e.code&&v([pn])),"KeyF"===e.code&&v([fn]),"KeyG"===e.code&&v([vn]),"KeyM"===e.code&&v([un]),"KeyN"===e.code&&v([mn]),"KeyC"===e.code&&v([yn]))}));const v=e=>{l.push(e)};return{addEvent:v,consumeAll:()=>{if(0===l.length)return[];const e=l.slice();return l=[],e},createKeyEvents:()=>{const e=(s?1:0)-(i?1:0),t=(r?1:0)-(d?1:0);if(0!==e||0!==t){const o=(p?24:12)*Math.sqrt(n.getCurrentZoom()),l=n.viewportDimToWorld({w:e*o,h:t*o});v([en,l.w,l.h]),f&&(f[0]-=l.w,f[1]-=l.h)}if(c&&u);else if(c){if(n.canZoom("in")){const e=f||m();v([ln,...e])}}else if(u&&n.canZoom("out")){const e=f||m();v([an,...e])}},setHotkeys:e=>{a=e}}}(v,window,V,o),U=sl.getImageUrl(e),R=()=>{const t=sl.getStartTs(e),n=sl.getFinishTs(e),o=C();a.setFinished(!!n),a.setDuration((n||o)-t)};R(),a.setPiecesDone(sl.getFinishedPiecesCount(e)),a.setPiecesTotal(sl.getPieceCount(e));const G=C();a.setActivePlayers(sl.getActivePlayers(e,G)),a.setIdlePlayers(sl.getIdlePlayers(e,G));const F=!!sl.getFinishTs(e);let j=F;const W=()=>j&&!F,H=()=>kl(hl,100),K=()=>Al(ml,!1),q=()=>Al(wl,!0),Y=()=>{const e=H();i.volume=e/100,i.play()},Q=()=>o===El?zl(yl,"#222222"):sl.getPlayerBgColor(e,t)||zl(yl,"#222222"),Z=()=>o===El?zl(fl,"#ffffff"):sl.getPlayerColor(e,t)||zl(fl,"#ffffff");let X="",J="",ee=!1;const te=e=>{ee=e;const[t,n]=e?[X,"grab"]:[J,"default"];v.style.cursor=`url('${t}') ${g} ${m}, ${n}`},ne=e=>{X=co.colorizedCanvas(r,c,e).toDataURL(),J=co.colorizedCanvas(d,u,e).toDataURL(),te(ee)};ne(Z());const oe=()=>{a.setReplaySpeed&&a.setReplaySpeed(w.speeds[w.speedIdx]),a.setReplayPaused&&a.setReplayPaused(w.paused)},le=()=>{w.speedIdx+1{w.speedIdx>=1&&(w.speedIdx--,oe())},ie=()=>{w.paused=!w.paused,oe()},re=[];let de;let ce;if("play"===o?re.push(setInterval((()=>{R()}),1e3)):o===El&&oe(),"play"===o)Nn.onServerChange((n=>{n[0],n[1],n[2];const o=n[3];for(const[l,a]of o)switch(l){case Cn:{const n=ae.decodePlayer(a);n.id!==t&&(sl.setPlayer(e,n.id,n),Nl=!0)}break;case bn:{const t=ae.decodePiece(a);sl.setPiece(e,t.idx,t),Nl=!0}break;case wn:sl.setPuzzleData(e,a),Nl=!0}j=!!sl.getFinishTs(e)}));else if(o===El){const t=(t,n)=>{const o=t;if(o[0]===Zt){const t=o[1];return sl.addPlayer(e,t,n),!0}if(o[0]===Xt){const t=sl.getPlayerIdByIndex(e,o[1]);if(!t)throw"[ 2021-05-17 player not found (update player) ]";return sl.addPlayer(e,t,n),!0}if(o[0]===Jt){const t=sl.getPlayerIdByIndex(e,o[1]);if(!t)throw"[ 2021-05-17 player not found (handle input) ]";const l=o[2];return sl.handleInput(e,t,l,n),!0}return!1};let n=w.lastGameTs;const o=async()=>{w.logPointer+1>=w.log.length&&await b(e);const l=L();if(w.paused)return w.lastRealTs=l,void(de=setTimeout(o,50));const a=(l-w.lastRealTs)*w.speeds[w.speedIdx];let s=w.lastGameTs+a;for(;;){if(w.paused)break;const e=w.logPointer+1;if(e>=w.log.length)break;const o=w.log[w.logPointer],l=n+o[o.length-1],a=w.log[e],i=a[a.length-1],r=l+i;if(r>s){s+500*${let t=!1;const n=e.fps||60,o=e.slow||1,l=e.update,a=e.render,s=window.requestAnimationFrame,i=1/n,r=o*i;let d,c=0,u=window.performance.now();const p=()=>{for(d=window.performance.now(),c+=Math.min(1,(d-u)/1e3);c>r;)c-=r,l(i);a(c/o),u=d,t||s(p)};return s(p),{stop:()=>{t=!0}}})({update:()=>{B.createKeyEvents();for(const n of B.consumeAll())if("play"===o){const o=n[0];if(o===en){const e=n[1],t=n[2],o=V.worldDimToViewport({w:e,h:t});Nl=!0,V.move(o.w,o.h)}else if(o===on){if(ue&&!sl.getFirstOwnedPiece(e,t)){const e={x:n[1],y:n[2]},t=V.worldToViewport(e),o=Math.round(t.x-ue.x),l=Math.round(t.y-ue.y);Nl=!0,V.move(o,l),ue=t}}else if(o===rn)ne(n[1]);else if(o===tn){const e={x:n[1],y:n[2]};ue=V.worldToViewport(e),te(!0)}else if(o===nn)ue=null,te(!1);else if(o===ln){const e={x:n[1],y:n[2]};Nl=!0,V.zoom("in",V.worldToViewport(e))}else if(o===an){const e={x:n[1],y:n[2]};Nl=!0,V.zoom("out",V.worldToViewport(e))}else o===cn?a.togglePreview():o===un?a.toggleSoundsEnabled():o===mn?a.togglePlayerNames():o===yn?O():o===fn?(Ml=!Ml,Nl=!0):o===vn&&(Dl=!Dl,Nl=!0);const l=C();sl.handleInput(e,t,n,l,(e=>{K()&&Y()})).length>0&&(Nl=!0),Nn.sendClientEvent(n)}else if(o===El){const e=n[0];if(e===pn)ie();else if(e===hn)se();else if(e===gn)le();else if(e===en){const e=n[1],t=n[2];Nl=!0,V.move(e,t)}else if(e===on){if(ue){const e={x:n[1],y:n[2]},t=V.worldToViewport(e),o=Math.round(t.x-ue.x),l=Math.round(t.y-ue.y);Nl=!0,V.move(o,l),ue=t}}else if(e===rn)ne(n[1]);else if(e===tn){const e={x:n[1],y:n[2]};ue=V.worldToViewport(e),te(!0)}else if(e===nn)ue=null,te(!1);else if(e===ln){const e={x:n[1],y:n[2]};Nl=!0,V.zoom("in",V.worldToViewport(e))}else if(e===an){const e={x:n[1],y:n[2]};Nl=!0,V.zoom("out",V.worldToViewport(e))}else e===cn?a.togglePreview():e===un?a.toggleSoundsEnabled():e===mn?a.togglePlayerNames():e===yn?O():e===fn?(Ml=!Ml,Nl=!0):e===vn&&(Dl=!Dl,Nl=!0)}j=!!sl.getFinishTs(e),W()&&(N.update(),Nl=!0)},render:async()=>{if(!Nl)return;const n=C();let l,s,i;window.DEBUG&&ho(0),_.fillStyle=Q(),_.fillRect(0,0,v.width,v.height),window.DEBUG&&mo("clear done"),l=V.worldToViewportRaw(I),s=V.worldDimToViewportRaw(E),_.fillStyle="rgba(255, 255, 255, .3)",_.fillRect(l.x,l.y,s.w,s.h),window.DEBUG&&mo("board done");const r=sl.getPiecesSortedByZIndex(e);window.DEBUG&&mo("get tiles done"),s=V.worldDimToViewportRaw(M);for(const e of r)(-1===e.owner?Ml:Dl)&&(i=D[e.idx],l=V.worldToViewportRaw({x:k+e.pos.x,y:k+e.pos.y}),_.drawImage(i,0,0,i.width,i.height,l.x,l.y,s.w,s.h));window.DEBUG&&mo("tiles done");const d=[];for(const a of sl.getActivePlayers(e,n))c=a,(o===El||c.id!==t)&&(i=await f(a),l=V.worldToViewport(a),_.drawImage(i,l.x-g,l.y-m),q()&&d.push([`${a.name} (${a.points})`,l.x,l.y+h]));var c;_.fillStyle="white",_.textAlign="center";for(const[e,t,o]of d)_.fillText(e,t,o);window.DEBUG&&mo("players done"),a.setActivePlayers(sl.getActivePlayers(e,n)),a.setIdlePlayers(sl.getIdlePlayers(e,n)),a.setPiecesDone(sl.getFinishedPiecesCount(e)),window.DEBUG&&mo("HUD done"),W()&&N.render(),Nl=!1}}),{setHotkeys:e=>{B.setHotkeys(e)},onBgChange:e=>{Sl(yl,e),B.addEvent([sn,e])},onColorChange:e=>{Sl(fl,e),B.addEvent([rn,e])},onNameChange:e=>{Sl(vl,e),B.addEvent([dn,e])},onSoundsEnabledChange:e=>{Pl(ml,e)},onSoundsVolumeChange:e=>{xl(hl,e),Y()},onShowPlayerNamesChange:e=>{Pl(wl,e)},replayOnSpeedUp:le,replayOnSpeedDown:se,replayOnPauseToggle:ie,previewImageUrl:U,player:{background:Q(),color:Z(),name:o===El?zl(vl,"anon"):sl.getPlayerName(e,t)||zl(vl,"anon"),soundsEnabled:K(),soundsVolume:H(),showPlayerNames:q()},game:sl.get(e),disconnect:Nn.disconnect,connect:x,unload:()=>{re.forEach((e=>{clearInterval(e)})),de&&clearTimeout(de),ce&&ce.stop()}}}var Vl=e({name:"game",components:{PuzzleStatus:zt,Scores:xt,SettingsOverlay:It,PreviewOverlay:Rt,InfoOverlay:Gt,ConnectionOverlay:_n,HelpOverlay:$n},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,cuttingPuzzle:!0,g:{player:{background:"",color:"",name:"",soundsEnabled:!1,soundsVolume:100,showPlayerNames:!0},game:null,previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},onSoundsEnabledChange:e=>{},onSoundsVolumeChange:e=>{},onShowPlayerNamesChange:e=>{},connect:()=>{},disconnect:()=>{},unload:()=>{}}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.$watch((()=>this.g.player.soundsEnabled),(e=>{this.g.onSoundsEnabledChange(e)})),this.$watch((()=>this.g.player.soundsVolume),(e=>{this.g.onSoundsVolumeChange(e)})),this.$watch((()=>this.g.player.showPlayerNames),(e=>{this.g.onShowPlayerNamesChange(e)})),this.g=await _l(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,"play",this.$el,{setPuzzleCut:()=>{this.cuttingPuzzle=!1},setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},togglePreview:()=>{this.toggle("preview",!1)},setConnectionState:e=>{this.connectionState=e},toggleSoundsEnabled:()=>{this.g.player.soundsEnabled=!this.g.player.soundsEnabled},togglePlayerNames:()=>{this.g.player.showPlayerNames=!this.g.player.showPlayerNames}}))},unmounted(){this.g.unload(),this.g.disconnect()},methods:{reconnect(){this.g.connect()},toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}}});const Ol={id:"game"},Bl={key:1,class:"overlay"},Ul=n("div",{class:"overlay-content"},[n("div",null,"⏳ Cutting puzzle, please wait... ⏳")],-1),Rl={class:"menu"},$l={class:"tabs"},Gl=i("🧩 Puzzles");Vl.render=function(e,i,r,d,c,u){const g=a("settings-overlay"),h=a("preview-overlay"),m=a("info-overlay"),y=a("help-overlay"),f=a("connection-overlay"),v=a("puzzle-status"),w=a("router-link"),b=a("scores");return s(),t("div",Ol,[p(n(g,{onBgclick:i[1]||(i[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":i[2]||(i[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[x,"settings"===e.overlay]]),p(n(h,{onBgclick:i[3]||(i[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[x,"preview"===e.overlay]]),e.g.game?p((s(),t(m,{key:0,onBgclick:i[4]||(i[4]=t=>e.toggle("info",!0)),game:e.g.game},null,8,["game"])),[[x,"info"===e.overlay]]):l("",!0),p(n(y,{onBgclick:i[5]||(i[5]=t=>e.toggle("help",!0))},null,512),[[x,"help"===e.overlay]]),e.cuttingPuzzle?(s(),t("div",Bl,[Ul])):l("",!0),n(f,{connectionState:e.connectionState,onReconnect:e.reconnect},null,8,["connectionState","onReconnect"]),n(v,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},null,8,["finished","duration","piecesDone","piecesTotal"]),n("div",Rl,[n("div",$l,[n(w,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[Gl])),_:1}),n("div",{class:"opener",onClick:i[6]||(i[6]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:i[7]||(i[7]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:i[8]||(i[8]=t=>e.toggle("info",!0))},"ℹ️ Info"),n("div",{class:"opener",onClick:i[9]||(i[9]=t=>e.toggle("help",!0))},"⌨️ Hotkeys")])]),n(b,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])};var Ll=e({name:"replay",components:{PuzzleStatus:zt,Scores:xt,SettingsOverlay:It,PreviewOverlay:Rt,InfoOverlay:Gt,HelpOverlay:$n},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,cuttingPuzzle:!0,g:{player:{background:"",color:"",name:"",soundsEnabled:!1,soundsVolume:100,showPlayerNames:!0},game:null,previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},onSoundsEnabledChange:e=>{},onSoundsVolumeChange:e=>{},onShowPlayerNamesChange:e=>{},replayOnSpeedUp:()=>{},replayOnSpeedDown:()=>{},replayOnPauseToggle:()=>{},connect:()=>{},disconnect:()=>{},unload:()=>{}},replay:{speed:1,paused:!1}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.$watch((()=>this.g.player.soundsEnabled),(e=>{this.g.onSoundsEnabledChange(e)})),this.$watch((()=>this.g.player.soundsVolume),(e=>{this.g.onSoundsVolumeChange(e)})),this.$watch((()=>this.g.player.showPlayerNames),(e=>{this.g.onShowPlayerNamesChange(e)})),this.g=await _l(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,El,this.$el,{setPuzzleCut:()=>{this.cuttingPuzzle=!1},setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},togglePreview:()=>{this.toggle("preview",!1)},setConnectionState:e=>{this.connectionState=e},setReplaySpeed:e=>{this.replay.speed=e},setReplayPaused:e=>{this.replay.paused=e},toggleSoundsEnabled:()=>{this.g.player.soundsEnabled=!this.g.player.soundsEnabled},togglePlayerNames:()=>{this.g.player.showPlayerNames=!this.g.player.showPlayerNames}}))},unmounted(){this.g.unload(),this.g.disconnect()},methods:{toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}},computed:{replayText(){return"Replay-Speed: "+this.replay.speed+"x"+(this.replay.paused?" Paused":"")}}});const Fl={id:"replay"},jl={key:1,class:"overlay"},Wl=n("div",{class:"overlay-content"},[n("div",null,"⏳ Cutting puzzle, please wait... ⏳")],-1),Hl={class:"menu"},Kl={class:"tabs"},ql=i("🧩 Puzzles");Ll.render=function(e,i,d,c,u,g){const h=a("settings-overlay"),m=a("preview-overlay"),y=a("info-overlay"),f=a("help-overlay"),v=a("puzzle-status"),w=a("router-link"),b=a("scores");return s(),t("div",Fl,[p(n(h,{onBgclick:i[1]||(i[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":i[2]||(i[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[x,"settings"===e.overlay]]),p(n(m,{onBgclick:i[3]||(i[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[x,"preview"===e.overlay]]),e.g.game?p((s(),t(y,{key:0,onBgclick:i[4]||(i[4]=t=>e.toggle("info",!0)),game:e.g.game},null,8,["game"])),[[x,"info"===e.overlay]]):l("",!0),p(n(f,{onBgclick:i[5]||(i[5]=t=>e.toggle("help",!0))},null,512),[[x,"help"===e.overlay]]),e.cuttingPuzzle?(s(),t("div",jl,[Wl])):l("",!0),n(v,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},{default:o((()=>[n("div",null,[n("div",null,r(e.replayText),1),n("button",{class:"btn",onClick:i[6]||(i[6]=t=>e.g.replayOnSpeedUp())},"⏫"),n("button",{class:"btn",onClick:i[7]||(i[7]=t=>e.g.replayOnSpeedDown())},"⏬"),n("button",{class:"btn",onClick:i[8]||(i[8]=t=>e.g.replayOnPauseToggle())},"⏸️")])])),_:1},8,["finished","duration","piecesDone","piecesTotal"]),n("div",Hl,[n("div",Kl,[n(w,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[ql])),_:1}),n("div",{class:"opener",onClick:i[9]||(i[9]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:i[10]||(i[10]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:i[11]||(i[11]=t=>e.toggle("info",!0))},"ℹ️ Info"),n("div",{class:"opener",onClick:i[12]||(i[12]=t=>e.toggle("help",!0))},"⌨️ Hotkeys")])]),n(b,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])},(async()=>{const e=function(){let e=zl("ID","");return e||(e=ae.uniqId(),Sl("ID",e)),e}(),t=function(){let e=zl("SECRET","");return e||(e=ae.uniqId(),Sl("SECRET",e)),e}();O(e),B(t);const n=await _("/api/me",{}),o=await n.json(),l=await _("/api/conf",{}),a=await l.json(),s=k({history:P(),routes:[{name:"index",path:"/",component:Z},{name:"new-game",path:"/new-game",component:pt},{name:"game",path:"/g/:id",component:Vl},{name:"replay",path:"/replay/:id",component:Ll}]});s.beforeEach(((e,t)=>{t.name&&document.documentElement.classList.remove(`view-${String(t.name)}`),document.documentElement.classList.add(`view-${String(e.name)}`)}));const i=A(S);i.config.globalProperties.$me=o,i.config.globalProperties.$config=a,i.config.globalProperties.$clientId=e,i.use(s),i.mount("#app")})(); diff --git a/build/public/assets/index.7efa4c6c.js b/build/public/assets/index.7efa4c6c.js deleted file mode 100644 index 375a85b..0000000 --- a/build/public/assets/index.7efa4c6c.js +++ /dev/null @@ -1 +0,0 @@ -import{d as e,c as t,a as n,w as o,b as l,r as a,o as i,e as s,t as r,F as d,f as c,g as u,h as p,v as g,i as h,j as m,p as y,k as f,l as v,m as w,n as b,q as x,s as C,u as k,x as A,y as S}from"./vendor.684f7bc8.js";var z=e({name:"app",computed:{showNav(){return!["game","replay"].includes(String(this.$route.name))}}});const T={id:"app"},P={key:0,class:"nav"},I=s("Index"),D=s("New game");z.render=function(e,s,r,d,c,u){const p=a("router-link"),g=a("router-view");return i(),t("div",T,[e.showNav?(i(),t("ul",P,[n("li",null,[n(p,{class:"btn",to:{name:"index"}},{default:o((()=>[I])),_:1})]),n("li",null,[n(p,{class:"btn",to:{name:"new-game"}},{default:o((()=>[D])),_:1})])])):l("",!0),n(g)])};const E=864e5,_=e=>{const t=Math.floor(e/E);e%=E;const n=Math.floor(e/36e5);e%=36e5;const o=Math.floor(e/6e4);e%=6e4;return`${t}d ${n}h ${o}m ${Math.floor(e/1e3)}s`};var M=1e3,O=()=>{const e=new Date;return Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())},N=(e,t)=>_(t-e),B=_,U=e({name:"game-teaser",props:{game:{type:Object,required:!0}},computed:{style(){return{"background-image":`url("${this.game.imageUrl.replace("uploads/","uploads/r/")+"-375x210.webp"}")`}}},methods:{time(e,t){const n=t?"🏁":"⏳",o=e,l=t||O();return`${n} ${N(o,l)}`}}});const R={class:"game-info-text"},V=n("br",null,null,-1),$=n("br",null,null,-1),G=n("br",null,null,-1),F=s(" β†ͺ️ Watch replay ");U.render=function(e,d,c,u,p,g){const h=a("router-link");return i(),t("div",{class:"game-teaser",style:e.style},[n(h,{class:"game-info",to:{name:"game",params:{id:e.game.id}}},{default:o((()=>[n("span",R,[s(" 🧩 "+r(e.game.tilesFinished)+"/"+r(e.game.tilesTotal),1),V,s(" πŸ‘₯ "+r(e.game.players),1),$,s(" "+r(e.time(e.game.started,e.game.finished)),1),G])])),_:1},8,["to"]),e.game.hasReplay?(i(),t(h,{key:0,class:"game-replay",to:{name:"replay",params:{id:e.game.id}}},{default:o((()=>[F])),_:1},8,["to"])):l("",!0)],4)};var L=e({components:{GameTeaser:U},data:()=>({gamesRunning:[],gamesFinished:[]}),async created(){const e=await fetch("/api/index-data"),t=await e.json();this.gamesRunning=t.gamesRunning,this.gamesFinished=t.gamesFinished}});const j=n("h1",null,"Running games",-1),W=n("h1",null,"Finished games",-1);L.render=function(e,o,l,s,r,u){const p=a("game-teaser");return i(),t("div",null,[j,(i(!0),t(d,null,c(e.gamesRunning,((e,o)=>(i(),t("div",{class:"game-teaser-wrap",key:o},[n(p,{game:e},null,8,["game"])])))),128)),W,(i(!0),t(d,null,c(e.gamesFinished,((e,o)=>(i(),t("div",{class:"game-teaser-wrap",key:o},[n(p,{game:e},null,8,["game"])])))),128))])};var q=e({name:"image-teaser",props:{image:{type:Object,required:!0}},computed:{style(){return{backgroundImage:`url("${this.image.url.replace("uploads/","uploads/r/")+"-150x100.webp"}")`}}},emits:{click:null,editClick:null},methods:{onClick(){this.$emit("click")},onEditClick(){this.$emit("editClick")}}});q.render=function(e,o,l,a,s,r){return i(),t("div",{class:"imageteaser",style:e.style,onClick:o[2]||(o[2]=(...t)=>e.onClick&&e.onClick(...t))},[n("div",{class:"btn edit",onClick:o[1]||(o[1]=u(((...t)=>e.onEditClick&&e.onEditClick(...t)),["stop"]))},"✏️")],4)};var H,Y,Q,Z,K,X,J,ee,te=e({name:"image-library",components:{ImageTeaser:q},props:{images:{type:Array,required:!0}},emits:{imageClicked:null,imageEditClicked:null},methods:{imageClicked(e){this.$emit("imageClicked",e)},imageEditClicked(e){this.$emit("imageEditClicked",e)}}});te.render=function(e,n,o,l,s,r){const u=a("image-teaser");return i(),t("div",null,[(i(!0),t(d,null,c(e.images,((n,o)=>(i(),t(u,{image:n,onClick:t=>e.imageClicked(n),onEditClick:t=>e.imageEditClicked(n),key:o},null,8,["image","onClick","onEditClick"])))),128))])},(Y=H||(H={}))[Y.Flat=0]="Flat",Y[Y.Out=1]="Out",Y[Y.In=-1]="In",(Z=Q||(Q={}))[Z.FINAL=0]="FINAL",Z[Z.ANY=1]="ANY",(X=K||(K={}))[X.NORMAL=0]="NORMAL",X[X.ANY=1]="ANY",X[X.FLAT=2]="FLAT",(ee=J||(J={}))[ee.NORMAL=0]="NORMAL",ee[ee.REAL=1]="REAL";class ne{constructor(e){this.rand_high=e||3735929054,this.rand_low=1231121986^e}random(e,t){this.rand_high=(this.rand_high<<16)+(this.rand_high>>16)+this.rand_low&4294967295,this.rand_low=this.rand_low+this.rand_high&4294967295;return e+(this.rand_high>>>0)/4294967295*(t-e+1)|0}choice(e){return e[this.random(0,e.length-1)]}shuffle(e){const t=e.slice();for(let n=0;n<=t.length-2;n++){const e=this.random(n,t.length-1),o=t[n];t[n]=t[e],t[e]=o}return t}static serialize(e){return{rand_high:e.rand_high,rand_low:e.rand_low}}static unserialize(e){const t=new ne(0);return t.rand_high=e.rand_high,t.rand_low=e.rand_low,t}}const oe=(e,t)=>{const n=`${e}`;return n.length>=t.length?n:t.substr(0,t.length-n.length)+n},le=(...e)=>{const t=t=>(...n)=>{const o=new Date,l=oe(o.getHours(),"00"),a=oe(o.getMinutes(),"00"),i=oe(o.getSeconds(),"00");console[t](`${l}:${a}:${i}`,...e,...n)};return{log:t("log"),error:t("error"),info:t("info")}};var ae={hash:e=>{let t=0;for(let n=0;n{let t=e.toLowerCase();return t=t.replace(/[^a-z0-9]+/g,"-"),t=t.replace(/^-|-$/,""),t},uniqId:()=>Date.now().toString(36)+Math.random().toString(36).substring(2),encodeShape:function(e){return e.top+1<<0|e.right+1<<2|e.bottom+1<<4|e.left+1<<6},decodeShape:function(e){return{top:(e>>0&3)-1,right:(e>>2&3)-1,bottom:(e>>4&3)-1,left:(e>>6&3)-1}},encodePiece:function(e){return[e.idx,e.pos.x,e.pos.y,e.z,e.owner,e.group]},decodePiece:function(e){return{idx:e[0],pos:{x:e[1],y:e[2]},z:e[3],owner:e[4],group:e[5]}},encodePlayer:function(e){return[e.id,e.x,e.y,e.d,e.name,e.color,e.bgcolor,e.points,e.ts]},decodePlayer:function(e){return{id:e[0],x:e[1],y:e[2],d:e[3],name:e[4],color:e[5],bgcolor:e[6],points:e[7],ts:e[8]}},encodeGame:function(e){return[e.id,e.rng.type||"",ne.serialize(e.rng.obj),e.puzzle,e.players,e.evtInfos,e.scoreMode||Q.FINAL,e.shapeMode||K.ANY,e.snapMode||J.NORMAL]},decodeGame:function(e){return{id:e[0],rng:{type:e[1],obj:ne.unserialize(e[2])},puzzle:e[3],players:e[4],evtInfos:e[5],scoreMode:e[6],shapeMode:e[7],snapMode:e[8]}},coordByPieceIdx:function(e,t){const n=e.width/e.tileSize;return{x:t%n,y:Math.floor(t/n)}},asQueryArgs:function(e){const t=[];for(const n in e){const o=[n,e[n]].map(encodeURIComponent);t.push(o.join("="))}return 0===t.length?"":`?${t.join("&")}`}};const ie={name:"responsive-image",props:{src:String,title:{type:String,default:""},height:{type:String,default:"100%"},width:{type:String,default:"100%"}},computed:{style(){return{display:"inline-block",verticalAlign:"text-bottom",backgroundImage:`url('${this.src}')`,backgroundRepeat:"no-repeat",backgroundSize:"contain",backgroundPosition:"center",width:this.width,height:this.height}}}};ie.render=function(e,n,o,l,a,s){return i(),t("div",{style:s.style,title:o.title},null,12,["title"])};var se=e({name:"tags-input",props:{modelValue:{type:Array,required:!0},autocompleteTags:{type:Function}},emits:{"update:modelValue":null},data:()=>({input:"",values:[],autocomplete:{idx:-1,values:[]}}),created(){this.values=this.modelValue},methods:{onKeyUp(e){return"ArrowDown"===e.key&&this.autocomplete.values.length>0?(this.autocomplete.idx0?(this.autocomplete.idx>0&&this.autocomplete.idx--,e.stopPropagation(),!1):","===e.key?(this.add(),e.stopPropagation(),!1):void(this.input&&this.autocompleteTags?(this.autocomplete.values=this.autocompleteTags(this.input,this.values),this.autocomplete.idx=-1):(this.autocomplete.values=[],this.autocomplete.idx=-1))},addVal(e){const t=e.replace(/,/g,"").trim();t&&(this.values.includes(t)||this.values.push(t),this.input="",this.autocomplete.values=[],this.autocomplete.idx=-1,this.$emit("update:modelValue",this.values),this.$refs.input.focus())},add(){const e=this.autocomplete.idx>=0?this.autocomplete.values[this.autocomplete.idx]:this.input;this.addVal(e)},rm(e){this.values=this.values.filter((t=>t!==e)),this.$emit("update:modelValue",this.values)}}});const re=m();y("data-v-39ed99c7");const de={key:0,class:"autocomplete"};f();const ce=re(((e,o,a,s,u,m)=>(i(),t("div",null,[p(n("input",{ref:"input",class:"input",type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.input=t),placeholder:"Plants, People",onChange:o[2]||(o[2]=(...t)=>e.onChange&&e.onChange(...t)),onKeydown:o[3]||(o[3]=h(((...t)=>e.add&&e.add(...t)),["enter"])),onKeyup:o[4]||(o[4]=(...t)=>e.onKeyUp&&e.onKeyUp(...t))},null,544),[[g,e.input]]),e.autocomplete.values?(i(),t("div",de,[n("ul",null,[(i(!0),t(d,null,c(e.autocomplete.values,((n,o)=>(i(),t("li",{key:o,class:{active:o===e.autocomplete.idx},onClick:t=>e.addVal(n)},r(n),11,["onClick"])))),128))])])):l("",!0),(i(!0),t(d,null,c(e.values,((n,o)=>(i(),t("span",{key:o,class:"bit",onClick:t=>e.rm(n)},r(n)+" βœ–",9,["onClick"])))),128))]))));se.render=ce,se.__scopeId="data-v-39ed99c7";const ue=le("NewImageDialog.vue");var pe=e({name:"new-image-dialog",components:{ResponsiveImage:ie,TagsInput:se},props:{autocompleteTags:{type:Function}},emits:{bgclick:null,setupGameClick:null,postToGalleryClick:null},data:()=>({previewUrl:"",file:null,title:"",tags:[],droppable:!1}),computed:{canPostToGallery(){return!(!this.previewUrl||!this.file)},canSetupGameClick(){return!(!this.previewUrl||!this.file)}},methods:{imageFromDragEvt(e){var t;const n=null==(t=e.dataTransfer)?void 0:t.items;if(!n||0===n.length)return null;const o=n[0];return o.type.startsWith("image/")?o:null},onFileSelect(e){const t=e.target;if(!t.files)return;const n=t.files[0];n&&this.preview(n)},preview(e){const t=new FileReader;t.readAsDataURL(e),t.onload=t=>{this.previewUrl=t.target.result,this.file=e}},postToGallery(){this.$emit("postToGalleryClick",{file:this.file,title:this.title,tags:this.tags})},setupGameClick(){this.$emit("setupGameClick",{file:this.file,title:this.title,tags:this.tags})},onDrop(e){this.droppable=!1;const t=this.imageFromDragEvt(e);if(!t)return!1;const n=t.getAsFile();return!!n&&(this.file=n,this.preview(n),e.preventDefault(),!1)},onDragover(e){return!!this.imageFromDragEvt(e)&&(this.droppable=!0,e.preventDefault(),!1)},onDragleave(){ue.info("onDragleave"),this.droppable=!1}}});const ge={key:0,class:"has-image"},he={key:1},me={class:"upload"},ye=n("span",{class:"btn"},"Upload File",-1),fe={class:"area-settings"},ve=n("td",null,[n("label",null,"Title")],-1),we=n("tr",null,[n("td",{colspan:"2"},[n("div",{class:"hint"},"Feel free to leave a credit to the artist/photographer in the title :)")])],-1),be=n("td",null,[n("label",null,"Tags")],-1),xe={class:"area-buttons"},Ce=s("🧩 Post to gallery "),ke=n("br",null,null,-1),Ae=s(" + set up game");pe.render=function(e,o,l,s,r,d){const c=a("responsive-image"),h=a("tags-input");return i(),t("div",{class:"overlay new-image-dialog",onClick:o[11]||(o[11]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[10]||(o[10]=u((()=>{}),["stop"]))},[n("div",{class:["area-image",{"has-image":!!e.previewUrl,"no-image":!e.previewUrl,droppable:e.droppable}],onDrop:o[3]||(o[3]=(...t)=>e.onDrop&&e.onDrop(...t)),onDragover:o[4]||(o[4]=(...t)=>e.onDragover&&e.onDragover(...t)),onDragleave:o[5]||(o[5]=(...t)=>e.onDragleave&&e.onDragleave(...t))},[e.previewUrl?(i(),t("div",ge,[n("span",{class:"remove btn",onClick:o[1]||(o[1]=t=>e.previewUrl="")},"X"),n(c,{src:e.previewUrl},null,8,["src"])])):(i(),t("div",he,[n("label",me,[n("input",{type:"file",style:{display:"none"},onChange:o[2]||(o[2]=(...t)=>e.onFileSelect&&e.onFileSelect(...t)),accept:"image/*"},null,32),ye])]))],34),n("div",fe,[n("table",null,[n("tr",null,[ve,n("td",null,[p(n("input",{type:"text","onUpdate:modelValue":o[6]||(o[6]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[g,e.title]])])]),we,n("tr",null,[be,n("td",null,[n(h,{modelValue:e.tags,"onUpdate:modelValue":o[7]||(o[7]=t=>e.tags=t),autocompleteTags:e.autocompleteTags},null,8,["modelValue","autocompleteTags"])])])])]),n("div",xe,[n("button",{class:"btn",disabled:!e.canPostToGallery,onClick:o[8]||(o[8]=(...t)=>e.postToGallery&&e.postToGallery(...t))},"πŸ–ΌοΈ Post to gallery",8,["disabled"]),n("button",{class:"btn",disabled:!e.canSetupGameClick,onClick:o[9]||(o[9]=(...t)=>e.setupGameClick&&e.setupGameClick(...t))},[Ce,ke,Ae],8,["disabled"])])])])};var Se=e({name:"edit-image-dialog",components:{ResponsiveImage:ie,TagsInput:se},props:{image:{type:Object,required:!0},autocompleteTags:{type:Function}},emits:{bgclick:null,saveClick:null},data:()=>({title:"",tags:[]}),created(){this.title=this.image.title,this.tags=this.image.tags.map((e=>e.title))},methods:{saveImage(){this.$emit("saveClick",{id:this.image.id,title:this.title,tags:this.tags})}}});const ze={class:"area-image"},Te={class:"has-image"},Pe={class:"area-settings"},Ie=n("td",null,[n("label",null,"Title")],-1),De=n("tr",null,[n("td",{colspan:"2"},[n("div",{class:"hint"},"Feel free to leave a credit to the artist/photographer in the title :)")])],-1),Ee=n("td",null,[n("label",null,"Tags")],-1),_e={class:"area-buttons"};Se.render=function(e,o,l,s,r,d){const c=a("responsive-image"),h=a("tags-input");return i(),t("div",{class:"overlay edit-image-dialog",onClick:o[5]||(o[5]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[4]||(o[4]=u((()=>{}),["stop"]))},[n("div",ze,[n("div",Te,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",Pe,[n("table",null,[n("tr",null,[Ie,n("td",null,[p(n("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[g,e.title]])])]),De,n("tr",null,[Ee,n("td",null,[n(h,{modelValue:e.tags,"onUpdate:modelValue":o[2]||(o[2]=t=>e.tags=t),autocompleteTags:e.autocompleteTags},null,8,["modelValue","autocompleteTags"])])])])]),n("div",_e,[n("button",{class:"btn",onClick:o[3]||(o[3]=(...t)=>e.saveImage&&e.saveImage(...t))},"πŸ–ΌοΈ Save image")])])])};var Me=e({name:"new-game-dialog",components:{ResponsiveImage:ie},props:{image:{type:Object,required:!0}},emits:{newGame:null,bgclick:null},data:()=>({tiles:1e3,scoreMode:Q.ANY,shapeMode:K.NORMAL,snapMode:J.NORMAL}),methods:{onNewGameClick(){this.$emit("newGame",{tiles:this.tilesInt,image:this.image,scoreMode:this.scoreModeInt,shapeMode:this.shapeModeInt,snapMode:this.snapModeInt})}},computed:{canStartNewGame(){return!!(this.tilesInt&&this.image&&this.image.url&&[0,1].includes(this.scoreModeInt))},scoreModeInt(){return parseInt(`${this.scoreMode}`,10)},shapeModeInt(){return parseInt(`${this.shapeMode}`,10)},snapModeInt(){return parseInt(`${this.snapMode}`,10)},tilesInt(){return parseInt(`${this.tiles}`,10)}}});const Oe={class:"area-image"},Ne={class:"has-image"},Be={key:0,class:"image-title"},Ue={class:"area-settings"},Re=n("td",null,[n("label",null,"Pieces")],-1),Ve=n("td",null,[n("label",null,"Scoring: ")],-1),$e=s(" Any (Score when pieces are connected to each other or on final location)"),Ge=n("br",null,null,-1),Fe=s(" Final (Score when pieces are put to their final location)"),Le=n("td",null,[n("label",null,"Shapes: ")],-1),je=s(" Normal"),We=n("br",null,null,-1),qe=s(" Any (flat pieces can occur anywhere)"),He=n("br",null,null,-1),Ye=s(" Flat (all pieces flat on all sides)"),Qe=n("td",null,[n("label",null,"Snapping: ")],-1),Ze=s(" Normal (pieces snap to final destination and to each other)"),Ke=n("br",null,null,-1),Xe=s(" Real (pieces snap only to corners, already snapped pieces and to each other)"),Je={class:"area-buttons"};Me.render=function(e,o,s,d,c,h){const m=a("responsive-image");return i(),t("div",{class:"overlay new-game-dialog",onClick:o[11]||(o[11]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[10]||(o[10]=u((()=>{}),["stop"]))},[n("div",Oe,[n("div",Ne,[n(m,{src:e.image.url,title:e.image.title},null,8,["src","title"])]),e.image.title?(i(),t("div",Be,'"'+r(e.image.title)+'"',1)):l("",!0)]),n("div",Ue,[n("table",null,[n("tr",null,[Re,n("td",null,[p(n("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.tiles=t)},null,512),[[g,e.tiles]])])]),n("tr",null,[Ve,n("td",null,[n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[2]||(o[2]=t=>e.scoreMode=t),value:"1"},null,512),[[v,e.scoreMode]]),$e]),Ge,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[3]||(o[3]=t=>e.scoreMode=t),value:"0"},null,512),[[v,e.scoreMode]]),Fe])])]),n("tr",null,[Le,n("td",null,[n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[4]||(o[4]=t=>e.shapeMode=t),value:"0"},null,512),[[v,e.shapeMode]]),je]),We,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[5]||(o[5]=t=>e.shapeMode=t),value:"1"},null,512),[[v,e.shapeMode]]),qe]),He,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[6]||(o[6]=t=>e.shapeMode=t),value:"2"},null,512),[[v,e.shapeMode]]),Ye])])]),n("tr",null,[Qe,n("td",null,[n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[7]||(o[7]=t=>e.snapMode=t),value:"0"},null,512),[[v,e.snapMode]]),Ze]),Ke,n("label",null,[p(n("input",{type:"radio","onUpdate:modelValue":o[8]||(o[8]=t=>e.snapMode=t),value:"1"},null,512),[[v,e.snapMode]]),Xe])])])])]),n("div",Je,[n("button",{class:"btn",disabled:!e.canStartNewGame,onClick:o[9]||(o[9]=(...t)=>e.onNewGameClick&&e.onNewGameClick(...t))}," 🧩 Generate Puzzle ",8,["disabled"])])])])};var et=e({components:{ImageLibrary:te,NewImageDialog:pe,EditImageDialog:Se,NewGameDialog:Me},data:()=>({filters:{sort:"date_desc",tags:[]},images:[],tags:[],image:{id:0,filename:"",file:"",url:"",title:"",tags:[],created:0},dialog:""}),async created(){await this.loadImages()},computed:{relevantTags(){return this.tags.filter((e=>e.total>0))}},methods:{autocompleteTags(e,t){return this.tags.filter((n=>!t.includes(n.title)&&n.title.toLowerCase().startsWith(e.toLowerCase()))).slice(0,10).map((e=>e.title))},toggleTag(e){this.filters.tags.includes(e.slug)?this.filters.tags=this.filters.tags.filter((t=>t!==e.slug)):this.filters.tags.push(e.slug),this.filtersChanged()},async loadImages(){const e=await fetch(`/api/newgame-data${ae.asQueryArgs(this.filters)}`),t=await e.json();this.images=t.images,this.tags=t.tags},async filtersChanged(){await this.loadImages()},onImageClicked(e){this.image=e,this.dialog="new-game"},onImageEditClicked(e){this.image=e,this.dialog="edit-image"},async uploadImage(e){const t=new FormData;t.append("file",e.file,e.file.name),t.append("title",e.title),t.append("tags",e.tags);const n=await fetch("/api/upload",{method:"post",body:t});return await n.json()},async saveImage(e){const t=await fetch("/api/save-image",{method:"post",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify({id:e.id,title:e.title,tags:e.tags})});return await t.json()},async onSaveImageClick(e){await this.saveImage(e),this.dialog="",await this.loadImages()},async postToGalleryClick(e){await this.uploadImage(e),this.dialog="",await this.loadImages()},async setupGameClick(e){const t=await this.uploadImage(e);this.loadImages(),this.image=t,this.dialog="new-game"},async onNewGame(e){const t=await fetch("/api/newgame",{method:"post",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(e)});if(200===t.status){const e=await t.json();this.$router.push({name:"game",params:{id:e.id}})}}}});const tt={class:"upload-image-teaser"},nt=n("div",{class:"hint"},"(The image you upload will be added to the public gallery.)",-1),ot={key:0},lt=s(" Tags: "),at=s(" Sort by: "),it=n("option",{value:"date_desc"},"Newest first",-1),st=n("option",{value:"date_asc"},"Oldest first",-1),rt=n("option",{value:"alpha_asc"},"A-Z",-1),dt=n("option",{value:"alpha_desc"},"Z-A",-1);et.render=function(e,o,s,u,g,h){const m=a("image-library"),y=a("new-image-dialog"),f=a("edit-image-dialog"),v=a("new-game-dialog");return i(),t("div",null,[n("div",tt,[n("div",{class:"btn btn-big",onClick:o[1]||(o[1]=t=>e.dialog="new-image")},"Upload your image"),nt]),n("div",null,[e.tags.length>0?(i(),t("label",ot,[lt,(i(!0),t(d,null,c(e.relevantTags,((n,o)=>(i(),t("span",{class:["bit",{on:e.filters.tags.includes(n.slug)}],key:o,onClick:t=>e.toggleTag(n)},r(n.title)+" ("+r(n.total)+")",11,["onClick"])))),128))])):l("",!0),n("label",null,[at,p(n("select",{"onUpdate:modelValue":o[2]||(o[2]=t=>e.filters.sort=t),onChange:o[3]||(o[3]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[it,st,rt,dt],544),[[w,e.filters.sort]])])]),n(m,{images:e.images,onImageClicked:e.onImageClicked,onImageEditClicked:e.onImageEditClicked},null,8,["images","onImageClicked","onImageEditClicked"]),"new-image"===e.dialog?(i(),t(y,{key:0,autocompleteTags:e.autocompleteTags,onBgclick:o[4]||(o[4]=t=>e.dialog=""),onPostToGalleryClick:e.postToGalleryClick,onSetupGameClick:e.setupGameClick},null,8,["autocompleteTags","onPostToGalleryClick","onSetupGameClick"])):l("",!0),"edit-image"===e.dialog?(i(),t(f,{key:1,autocompleteTags:e.autocompleteTags,onBgclick:o[5]||(o[5]=t=>e.dialog=""),onSaveClick:e.onSaveImageClick,image:e.image},null,8,["autocompleteTags","onSaveClick","image"])):l("",!0),e.image&&"new-game"===e.dialog?(i(),t(v,{key:2,onBgclick:o[6]||(o[6]=t=>e.dialog=""),onNewGame:e.onNewGame,image:e.image},null,8,["onNewGame","image"])):l("",!0)])};var ct=e({name:"scores",props:{activePlayers:{type:Array,required:!0},idlePlayers:{type:Array,required:!0}},computed:{actives(){return this.activePlayers.sort(((e,t)=>t.points-e.points)),this.activePlayers},idles(){return this.idlePlayers.sort(((e,t)=>t.points-e.points)),this.idlePlayers}}});const ut={class:"scores"},pt=n("div",null,"Scores",-1),gt=n("td",null,"⚑",-1),ht=n("td",null,"πŸ’€",-1);ct.render=function(e,o,l,a,s,u){return i(),t("div",ut,[pt,n("table",null,[(i(!0),t(d,null,c(e.actives,((e,o)=>(i(),t("tr",{key:o,style:{color:e.color}},[gt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128)),(i(!0),t(d,null,c(e.idles,((e,o)=>(i(),t("tr",{key:o,style:{color:e.color}},[ht,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128))])])};var mt=e({name:"puzzle-status",props:{finished:{type:Boolean,required:!0},duration:{type:Number,required:!0},piecesDone:{type:Number,required:!0},piecesTotal:{type:Number,required:!0}},computed:{icon(){return this.finished?"🏁":"⏳"},durationStr(){return B(this.duration)}}});const yt={class:"timer"};mt.render=function(e,o,l,a,s,d){return i(),t("div",yt,[n("div",null," 🧩 "+r(e.piecesDone)+"/"+r(e.piecesTotal),1),n("div",null,r(e.icon)+" "+r(e.durationStr),1),b(e.$slots,"default")])};var ft=e({name:"settings-overlay",emits:{bgclick:null,"update:modelValue":null},props:{modelValue:Object},created(){this.$watch("modelValue",(e=>{this.$emit("update:modelValue",e)}),{deep:!0})}});const vt=n("td",null,[n("label",null,"Background: ")],-1),wt=n("td",null,[n("label",null,"Color: ")],-1),bt=n("td",null,[n("label",null,"Name: ")],-1),xt=n("td",null,[n("label",null,"Sounds: ")],-1);ft.render=function(e,o,l,a,s,r){return i(),t("div",{class:"overlay transparent",onClick:o[6]||(o[6]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content settings",onClick:o[5]||(o[5]=u((()=>{}),["stop"]))},[n("tr",null,[vt,n("td",null,[p(n("input",{type:"color","onUpdate:modelValue":o[1]||(o[1]=t=>e.modelValue.background=t)},null,512),[[g,e.modelValue.background]])])]),n("tr",null,[wt,n("td",null,[p(n("input",{type:"color","onUpdate:modelValue":o[2]||(o[2]=t=>e.modelValue.color=t)},null,512),[[g,e.modelValue.color]])])]),n("tr",null,[bt,n("td",null,[p(n("input",{type:"text",maxLength:"16","onUpdate:modelValue":o[3]||(o[3]=t=>e.modelValue.name=t)},null,512),[[g,e.modelValue.name]])])]),n("tr",null,[xt,n("td",null,[p(n("input",{type:"checkbox","onUpdate:modelValue":o[4]||(o[4]=t=>e.modelValue.soundsEnabled=t)},null,512),[[x,e.modelValue.soundsEnabled]])])])])])};var Ct=e({name:"preview-overlay",props:{img:String},emits:{bgclick:null},computed:{previewStyle(){return{backgroundImage:`url('${this.img}')`}}}});const kt={class:"preview"};Ct.render=function(e,o,l,a,s,r){return i(),t("div",{class:"overlay",onClick:o[1]||(o[1]=t=>e.$emit("bgclick"))},[n("div",kt,[n("div",{class:"img",style:e.previewStyle},null,4)])])};var At=1,St=4,zt=2,Tt=3,Pt=2,It=4,Dt=3,Et=9,_t=1,Mt=2,Ot=3,Nt=4,Bt=5,Ut=6,Rt=7,Vt=8,$t=10,Gt=11,Ft=1,Lt=2,jt=3;const Wt=le("Communication.js");let qt,Ht=[],Yt=e=>{Ht.push(e)},Qt=[],Zt=e=>{Qt.push(e)};let Kt=0;const Xt=e=>{Kt!==e&&(Kt=e,Zt(e))};function Jt(e){if(2===Kt)try{qt.send(JSON.stringify(e))}catch(t){Wt.info("unable to send message.. maybe because ws is invalid?")}}let en,tn;var nn={connect:function(e,t,n){return en=0,tn={},Xt(3),new Promise((o=>{qt=new WebSocket(e,n+"|"+t),qt.onopen=()=>{Xt(2),Jt([Tt])},qt.onmessage=e=>{const t=JSON.parse(e.data),l=t[0];if(l===St){const e=t[1];o(e)}else{if(l!==At)throw`[ 2021-05-09 invalid connect msgType ${l} ]`;{const e=t[1],o=t[2];if(e===n&&tn[o])return void delete tn[o];Yt(t)}}},qt.onerror=()=>{throw Xt(1),"[ 2021-05-15 onerror ]"},qt.onclose=e=>{4e3===e.code||1001===e.code?Xt(4):Xt(1)}}))},requestReplayData:async function(e,t,n){const o={gameId:e,offset:t,size:n},l=await fetch(`/api/replay-data${ae.asQueryArgs(o)}`);return await l.json()},disconnect:function(){qt&&qt.close(4e3),en=0,tn={}},sendClientEvent:function(e){en++,tn[en]=e,Jt([zt,en,tn[en]])},onServerChange:function(e){Yt=e;for(const t of Ht)Yt(t);Ht=[]},onConnectionStateChange:function(e){Zt=e;for(const t of Qt)Zt(t);Qt=[]},CODE_CUSTOM_DISCONNECT:4e3,CONN_STATE_NOT_CONNECTED:0,CONN_STATE_DISCONNECTED:1,CONN_STATE_CLOSED:4,CONN_STATE_CONNECTED:2,CONN_STATE_CONNECTING:3},on=e({name:"connection-overlay",emits:{reconnect:null},props:{connectionState:Number},computed:{lostConnection(){return this.connectionState===nn.CONN_STATE_DISCONNECTED},connecting(){return this.connectionState===nn.CONN_STATE_CONNECTING},show(){return!(!this.lostConnection&&!this.connecting)}}});const ln={key:0,class:"overlay connection-lost"},an={key:0,class:"overlay-content"},sn=n("div",null,"⁉️ LOST CONNECTION ⁉️",-1),rn={key:1,class:"overlay-content"},dn=n("div",null,"Connecting...",-1);on.render=function(e,o,a,s,r,d){return e.show?(i(),t("div",ln,[e.lostConnection?(i(),t("div",an,[sn,n("span",{class:"btn",onClick:o[1]||(o[1]=t=>e.$emit("reconnect"))},"Reconnect")])):l("",!0),e.connecting?(i(),t("div",rn,[dn])):l("",!0)])):l("",!0)};var cn=e({name:"help-overlay",emits:{bgclick:null}});const un=n("tr",null,[n("td",null,"⬆️ Move up:"),n("td",null,[n("div",null,[n("kbd",null,"W"),s("/"),n("kbd",null,"↑"),s("/πŸ–±οΈ")])])],-1),pn=n("tr",null,[n("td",null,"⬇️ Move down:"),n("td",null,[n("div",null,[n("kbd",null,"S"),s("/"),n("kbd",null,"↓"),s("/πŸ–±οΈ")])])],-1),gn=n("tr",null,[n("td",null,"⬅️ Move left:"),n("td",null,[n("div",null,[n("kbd",null,"A"),s("/"),n("kbd",null,"←"),s("/πŸ–±οΈ")])])],-1),hn=n("tr",null,[n("td",null,"➑️ Move right:"),n("td",null,[n("div",null,[n("kbd",null,"D"),s("/"),n("kbd",null,"β†’"),s("/πŸ–±οΈ")])])],-1),mn=n("tr",null,[n("td"),n("td",null,[n("div",null,[s("Move faster by holding "),n("kbd",null,"Shift")])])],-1),yn=n("tr",null,[n("td",null,"πŸ”+ Zoom in:"),n("td",null,[n("div",null,[n("kbd",null,"E"),s("/πŸ–±οΈ-Wheel")])])],-1),fn=n("tr",null,[n("td",null,"πŸ”- Zoom out:"),n("td",null,[n("div",null,[n("kbd",null,"Q"),s("/πŸ–±οΈ-Wheel")])])],-1),vn=n("tr",null,[n("td",null,"πŸ–ΌοΈ Toggle preview:"),n("td",null,[n("div",null,[n("kbd",null,"Space")])])],-1),wn=n("tr",null,[n("td",null,"πŸ§©βœ”οΈ Toggle fixed pieces:"),n("td",null,[n("div",null,[n("kbd",null,"F")])])],-1),bn=n("tr",null,[n("td",null,"πŸ§©β“ Toggle loose pieces:"),n("td",null,[n("div",null,[n("kbd",null,"G")])])],-1),xn=n("tr",null,[n("td",null,"πŸ”‰ Toggle sounds:"),n("td",null,[n("div",null,[n("kbd",null,"M")])])],-1);cn.render=function(e,o,l,a,s,r){return i(),t("div",{class:"overlay transparent",onClick:o[2]||(o[2]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content help",onClick:o[1]||(o[1]=u((()=>{}),["stop"]))},[un,pn,gn,hn,mn,yn,fn,vn,wn,bn,xn])])};var Cn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"/assets/click.bb97cb07.mp3"}),kn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAW0lEQVQ4je1RywrAIAxLxP//5exixRWlVgZelpOKeTQFfnDypgy3eLIkSLLL8mxGPoHsU2hPAgDHBLvRX6hZZw/fwT0BtlLSONqCbWAmEIqMZOCDDlaDR3N03gOyDCn+y4DWmAAAAABJRU5ErkJggg=="}),An=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARElEQVQ4jWNgGAU0Af+hmBCbgYGBgYERhwHEAEYGBgYGJtIdiApYyLAZBVDsAqoagC1ACQJyY4ERg0GCISh6KA4DigEAou8LC+LnIJoAAAAASUVORK5CYII="}),Sn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVQ4ja1TQQ7AIAgD///n7jCozA2Hbk00jbG1KIrcARszTugoBs49qioZj7r2kKACptkyAOCJsJuA+GzglwHjvMSSWFVaENWVASxh5eRLiq5fN/ASjI89VcP2K3hHpq1cEXNaMfnrL3TDZP2tDuoOA6MzCCXWr38AAAAASUVORK5CYII="}),zn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAU0lEQVQ4jWNgoAH4D8X42HDARKlt5BoAd82AuQAOGLGIYQQUPv0wF5CiCQUge4EsQ5C9QI4BjMguwBYeBAElscCIy1ZivMKIwSDBEBQ9FCckigEAU3QOD7TGvY4AAAAASUVORK5CYII="});function Tn(){let e=0,t=0,n=1;const o=(o,l)=>{e+=o/n,t+=l/n},l=e=>{const t=n+.05*n*("in"===e?1:-1);return Math.min(Math.max(t,.1),6)},a=o=>({x:o.x/n-e,y:o.y/n-t}),i=o=>({x:(o.x+e)*n,y:(o.y+t)*n}),s=e=>({w:e.w*n,h:e.h*n}),r=e=>({w:e.w/n,h:e.h/n});return{getCurrentZoom:()=>n,move:o,canZoom:e=>n!=l(e),zoom:(e,t)=>((e,t)=>{if(n==e)return!1;const l=1-n/e;return o(-t.x*l,-t.y*l),n=e,!0})(l(e),t),worldToViewport:e=>{const{x:t,y:n}=i(e);return{x:Math.round(t),y:Math.round(n)}},worldToViewportRaw:i,worldDimToViewport:e=>{const{w:t,h:n}=s(e);return{w:Math.round(t),h:Math.round(n)}},worldDimToViewportRaw:s,viewportToWorld:e=>{const{x:t,y:n}=a(e);return{x:Math.round(t),y:Math.round(n)}},viewportToWorldRaw:a,viewportDimToWorld:e=>{const{w:t,h:n}=r(e);return{w:Math.round(t),h:Math.round(n)}},viewportDimToWorldRaw:r}}function Pn(e=0,t=0){const n=document.createElement("canvas");return n.width=e,n.height=t,n}var In={createCanvas:Pn,loadImageToBitmap:async function(e){return new Promise((t=>{const n=new Image;n.onload=()=>{createImageBitmap(n).then(t)},n.src=e}))},resizeBitmap:async function(e,t,n){const o=Pn(t,n);return o.getContext("2d").drawImage(e,0,0,e.width,e.height,0,0,t,n),await createImageBitmap(o)},colorizedCanvas:function(e,t,n){const o=Pn(e.width,e.height),l=o.getContext("2d");return l.save(),l.drawImage(t,0,0),l.fillStyle=n,l.globalCompositeOperation="source-in",l.fillRect(0,0,t.width,t.height),l.restore(),l.save(),l.globalCompositeOperation="destination-over",l.drawImage(e,0,0),l.restore(),o}};const Dn=le("Debug.js");let En=0,_n=0;var Mn=e=>{En=performance.now(),_n=e},On=e=>{const t=performance.now(),n=t-En;n>_n&&Dn.log(e+": "+n),En=t};function Nn(e,t){const n=e.x-t.x,o=e.y-t.y;return Math.sqrt(n*n+o*o)}function Bn(e){return{x:e.x+e.w/2,y:e.y+e.h/2}}var Un={pointSub:function(e,t){return{x:e.x-t.x,y:e.y-t.y}},pointAdd:function(e,t){return{x:e.x+t.x,y:e.y+t.y}},pointDistance:Nn,pointInBounds:function(e,t){return e.x>=t.x&&e.x<=t.x+t.w&&e.y>=t.y&&e.y<=t.y+t.h},rectCenter:Bn,rectMoved:function(e,t,n){return{x:e.x+t,y:e.y+n,w:e.w,h:e.h}},rectCenterDistance:function(e,t){return Nn(Bn(e),Bn(t))},rectsOverlap:function(e,t){return!(t.x>e.x+e.w||e.x>t.x+t.w||t.y>e.y+e.h||e.y>t.y+t.h)}};const Rn=le("PuzzleGraphics.js");function Vn(e,t){const n=ae.coordByPieceIdx(e,t);return{x:n.x*e.tileSize,y:n.y*e.tileSize,w:e.tileSize,h:e.tileSize}}var $n={loadPuzzleBitmaps:async function(e){const t=await In.loadImageToBitmap(e.info.imageUrl),n=await In.resizeBitmap(t,e.info.width,e.info.height);return await async function(e,t,n){Rn.log("start createPuzzleTileBitmaps");const o=n.tileSize,l=n.tileMarginWidth,a=n.tileDrawSize,i=o/100,s=[0,0,40,15,37,5,37,5,40,0,38,-5,38,-5,20,-20,50,-20,50,-20,80,-20,62,-5,62,-5,60,0,63,5,63,5,65,15,100,0],r=new Array(t.length),d={};function c(e){const t=`${e.top}${e.right}${e.left}${e.bottom}`;if(d[t])return d[t];const n=new Path2D,a={x:l,y:l},r=Un.pointAdd(a,{x:o,y:0}),c=Un.pointAdd(r,{x:0,y:o}),u=Un.pointSub(c,{x:o,y:0});if(n.moveTo(a.x,a.y),0!==e.top)for(let o=0;oae.decodePiece(Gn[e].puzzle.tiles[t]),to=(e,t)=>eo(e,t).group,no=(e,t)=>{const n=Gn[e].puzzle.info;return 0===t||t===n.tilesX-1||t===n.tiles-n.tilesX||t===n.tiles-1},oo=(e,t)=>{const n=Gn[e].puzzle.info,o={x:(n.table.width-n.width)/2,y:(n.table.height-n.height)/2},l=function(e,t){const n=Gn[e].puzzle.info,o=ae.coordByPieceIdx(n,t),l=o.x*n.tileSize,a=o.y*n.tileSize;return{x:l,y:a}}(e,t);return Un.pointAdd(o,l)},lo=(e,t)=>eo(e,t).pos,ao=e=>{const t=ko(e),n=Ao(e),o=Math.round(t/4),l=Math.round(n/4);return{x:0-o,y:0-l,w:t+2*o,h:n+2*l}},io=(e,t)=>{const n=uo(e),o=eo(e,t);return{x:o.pos.x,y:o.pos.y,w:n,h:n}},so=(e,t)=>eo(e,t).z,ro=(e,t)=>{for(const n of Gn[e].puzzle.tiles){const e=ae.decodePiece(n);if(e.owner===t)return e.idx}return-1},co=e=>Gn[e].puzzle.info.tileDrawSize,uo=e=>Gn[e].puzzle.info.tileSize,po=e=>Gn[e].puzzle.data.maxGroup,go=e=>Gn[e].puzzle.data.maxZ;function ho(e,t){const n=Gn[e].puzzle.info,o=ae.coordByPieceIdx(n,t);return[o.y>0?t-n.tilesX:-1,o.x0?t-1:-1]}const mo=(e,t,n)=>{for(const o of t)Jn(e,o,{z:n})},yo=(e,t,n)=>{const o=lo(e,t);Jn(e,t,{pos:Un.pointAdd(o,n)})},fo=(e,t,n)=>{const o=co(e),l=ao(e),a=n;for(const i of t){const t=eo(e,i);t.pos.x+n.xl.x+l.w&&(a.x=Math.min(l.x+l.w-t.pos.x+o,a.x)),t.pos.y+n.yl.y+l.h&&(a.y=Math.min(l.y+l.h-t.pos.y+o,a.y))}for(const i of t)yo(e,i,a)},vo=(e,t)=>eo(e,t).owner,wo=(e,t)=>{for(const n of t)Jn(e,n,{owner:-1,z:1})},bo=(e,t,n)=>{for(const o of t)Jn(e,o,{owner:n})};function xo(e,t){const n=Gn[e].puzzle.tiles,o=ae.decodePiece(n[t]),l=[];if(o.group)for(const a of n){const e=ae.decodePiece(a);e.group===o.group&&l.push(e.idx)}else l.push(o.idx);return l}const Co=(e,t)=>{const n=Ln(e,t);return n?n.points:0},ko=e=>Gn[e].puzzle.info.table.width,Ao=e=>Gn[e].puzzle.info.table.height;var So={setGame:function(e,t){Gn[e]=t},exists:function(e){return!!Gn[e]||!1},playerExists:Wn,getActivePlayers:function(e,t){const n=t-30*M;return qn(e).filter((e=>e.ts>=n))},getIdlePlayers:function(e,t){const n=t-30*M;return qn(e).filter((e=>e.ts0))},addPlayer:function(e,t,n){Wn(e,t)?Kn(e,t,{ts:n}):jn(e,t,function(e,t){return{id:e,x:0,y:0,d:0,name:null,color:null,bgcolor:null,points:0,ts:t}}(t,n))},getFinishedPiecesCount:Zn,getPieceCount:Hn,getImageUrl:function(e){return Gn[e].puzzle.info.imageUrl},setImageUrl:function(e,t){Gn[e].puzzle.info.imageUrl=t},get:function(e){return Gn[e]||null},getAllGames:function(){return Object.values(Gn).sort(((e,t)=>Qn(e.id)===Qn(t.id)?t.puzzle.data.started-e.puzzle.data.started:Qn(e.id)?1:-1))},getPlayerBgColor:(e,t)=>{const n=Ln(e,t);return n?n.bgcolor:null},getPlayerColor:(e,t)=>{const n=Ln(e,t);return n?n.color:null},getPlayerName:(e,t)=>{const n=Ln(e,t);return n?n.name:null},getPlayerIndexById:Fn,getPlayerIdByIndex:function(e,t){return Gn[e].players.length>t?ae.decodePlayer(Gn[e].players[t]).id:null},changePlayer:Kn,setPlayer:jn,setPiece:function(e,t,n){Gn[e].puzzle.tiles[t]=ae.encodePiece(n)},setPuzzleData:function(e,t){Gn[e].puzzle.data=t},getTableWidth:ko,getTableHeight:Ao,getPuzzle:e=>Gn[e].puzzle,getRng:e=>Gn[e].rng.obj,getPuzzleWidth:e=>Gn[e].puzzle.info.width,getPuzzleHeight:e=>Gn[e].puzzle.info.height,getPiecesSortedByZIndex:function(e){return Gn[e].puzzle.tiles.map(ae.decodePiece).sort(((e,t)=>e.z-t.z))},getFirstOwnedPiece:(e,t)=>{const n=ro(e,t);return n<0?null:Gn[e].puzzle.tiles[n]},getPieceDrawOffset:e=>Gn[e].puzzle.info.tileDrawOffset,getPieceDrawSize:co,getFinalPiecePos:oo,getStartTs:e=>Gn[e].puzzle.data.started,getFinishTs:e=>Gn[e].puzzle.data.finished,handleInput:function(e,t,n,o,l){const a=Gn[e].puzzle,i=function(e,t){return t in Gn[e].evtInfos?Gn[e].evtInfos[t]:{_last_mouse:null,_last_mouse_down:null}}(e,t),s=[],r=()=>{s.push([Ft,a.data])},d=t=>{s.push([Lt,ae.encodePiece(eo(e,t))])},c=e=>{for(const t of e)d(t)},u=()=>{const n=Ln(e,t);n&&s.push([jt,ae.encodePlayer(n)])},p=n[0];if(p===Ut){const l=n[1];Kn(e,t,{bgcolor:l,ts:o}),u()}else if(p===Rt){const l=n[1];Kn(e,t,{color:l,ts:o}),u()}else if(p===Vt){const l=`${n[1]}`.substr(0,16);Kn(e,t,{name:l,ts:o}),u()}else if(p===Et){const l=n[1],a=n[2],i=Ln(e,t);if(i){const n=i.x-l,s=i.y-a;Kn(e,t,{ts:o,x:n,y:s}),u()}}else if(p===_t){const l={x:n[1],y:n[2]};Kn(e,t,{d:1,ts:o}),u(),i._last_mouse_down=l;const a=((e,t)=>{const n=Gn[e].puzzle.info,o=Gn[e].puzzle.tiles;let l=-1,a=-1;for(let i=0;il)&&(l=e.z,a=i)}return a})(e,l);if(a>=0){const n=go(e)+1;Xn(e,{maxZ:n}),r();const o=xo(e,a);mo(e,o,go(e)),bo(e,o,t),c(o)}i._last_mouse=l}else if(p===Ot){const l=n[1],a=n[2],s={x:l,y:a};if(null===i._last_mouse_down)Kn(e,t,{x:l,y:a,ts:o}),u();else{const n=ro(e,t);if(n>=0){Kn(e,t,{x:l,y:a,ts:o}),u();const r=xo(e,n);let d=Un.pointInBounds(s,ao(e))&&Un.pointInBounds(i._last_mouse_down,ao(e));for(const t of r){const n=io(e,t);if(Un.pointInBounds(s,n)){d=!0;break}}if(d){const t=l-i._last_mouse_down.x,n=a-i._last_mouse_down.y;fo(e,r,{x:t,y:n}),c(r)}}else Kn(e,t,{ts:o}),u();i._last_mouse_down=s}i._last_mouse=s}else if(p===Mt){const s={x:n[1],y:n[2]},p=0;i._last_mouse_down=null;const g=ro(e,t);if(g>=0){const n=xo(e,g);bo(e,n,0),c(n);const i=lo(e,g),s=oo(e,g);let h=!1;if(function(e){return Gn[e].snapMode||J.NORMAL}(e)===J.REAL){for(const t of n)if(no(e,t)){h=!0;break}}else h=!0;if(h&&Un.pointDistance(s,i){const l=Gn[e].puzzle.info;if(n<0)return!1;if(((e,t,n)=>{const o=to(e,t),l=to(e,n);return!(!o||o!==l)})(e,t,n))return!1;const a=lo(e,t),i=Un.pointAdd(lo(e,n),{x:o[0]*l.tileSize,y:o[1]*l.tileSize});if(Un.pointDistance(a,i){const o=Gn[e].puzzle.tiles,l=to(e,t),a=to(e,n);let i;const s=[];l&&s.push(l),a&&s.push(a),l?i=l:a?i=a:(Xn(e,{maxGroup:po(e)+1}),r(),i=po(e));if(Jn(e,t,{group:i}),d(t),Jn(e,n,{group:i}),d(n),s.length>0)for(const r of o){const t=ae.decodePiece(r);s.includes(t.group)&&(Jn(e,t.idx,{group:i}),d(t.idx))}})(e,t,n),l=xo(e,t),((e,t)=>-1===vo(e,t))(e,n))wo(e,l);else{const t=((e,t)=>{let n=0;for(const o of t){const t=so(e,o);t>n&&(n=t)}return n})(e,l);mo(e,l,t)}return c(l),!0}return!1};let a=!1;for(const t of xo(e,g)){const o=ho(e,t);if(n(e,t,o[0],[0,1])||n(e,t,o[1],[-1,0])||n(e,t,o[2],[0,-1])||n(e,t,o[3],[1,0])){a=!0;break}}if(a&&Yn(e)===Q.ANY){const n=Co(e,t)+1;Kn(e,t,{d:p,ts:o,points:n}),u()}else Kn(e,t,{d:p,ts:o}),u();a&&l&&l(t)}}else Kn(e,t,{d:p,ts:o}),u();i._last_mouse=s}else if(p===Nt){const l=n[1],a=n[2];Kn(e,t,{x:l,y:a,ts:o}),u(),i._last_mouse={x:l,y:a}}else if(p===Bt){const l=n[1],a=n[2];Kn(e,t,{x:l,y:a,ts:o}),u(),i._last_mouse={x:l,y:a}}else Kn(e,t,{ts:o}),u();return function(e,t,n){Gn[e].evtInfos[t]=n}(e,t,i),s}};let zo=-10,To=20,Po=2,Io=15;class Do{constructor(e){this.radius=10,this.previousRadius=10,this.explodingDuration=100,this.hasExploded=!1,this.alive=!0,this.color=function(e){return"rgba("+e.random(0,255)+","+e.random(0,255)+","+e.random(0,255)+", 0.8)"}(e),this.px=window.innerWidth/4+Math.random()*window.innerWidth/2,this.py=window.innerHeight,this.vx=zo+Math.random()*To,this.vy=-1*(Po+Math.random()*Io),this.duration=0}update(e){if(this.hasExploded){const e=200-this.radius;this.previousRadius=this.radius,this.radius+=e/10,this.explodingDuration--,0==this.explodingDuration&&(this.alive=!1)}else this.vx+=0,this.vy+=1,this.vy>=0&&e&&this.explode(e),this.px+=this.vx,this.py+=this.vy}draw(e){e.beginPath(),e.arc(this.px,this.py,this.previousRadius,0,2*Math.PI,!1),this.hasExploded||(e.fillStyle=this.color,e.lineWidth=1,e.fill())}explode(e){this.hasExploded=!0;const t=3+Math.floor(3*Math.random());for(let n=0;n{this.resize()}))}setSpeedParams(){let e=0,t=0;for(;e=0;)t+=1,e+=t;Po=t/2,Io=t-Po;const n=1/4*this.canvas.width/(t/2);zo=-n,To=2*n}resize(){this.setSpeedParams()}init(){this.readyBombs=[],this.explodedBombs=[],this.particles=[];for(let e=0;e<1;e++)this.readyBombs.push(new Do(this.rng))}update(){100*Math.random()<5&&this.readyBombs.push(new Do(this.rng));const e=[];for(;this.explodedBombs.length>0;){const t=this.explodedBombs.shift();if(!t)break;t.update(),t.alive&&e.push(t)}this.explodedBombs=e;const t=[];for(;this.readyBombs.length>0;){const e=this.readyBombs.shift();if(!e)break;e.update(this.particles),e.hasExploded?this.explodedBombs.push(e):t.push(e)}this.readyBombs=t;const n=[];for(;this.particles.length>0;){const e=this.particles.shift();if(!e)break;e.update(),e.alive&&n.push(e)}this.particles=n}render(){this.ctx.beginPath(),this.ctx.fillStyle="rgba(0, 0, 0, 0.1)",this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);for(let e=0;e{const t=e.color+" "+e.d;if(!y[t]){const n=e.d?r:d;if(e.color){const o=e.d?c:u;y[t]=await createImageBitmap(In.colorizedCanvas(n,o,e.color))}else y[t]=n}return y[t]},v=function(e,t){return t.width=window.innerWidth,t.height=window.innerHeight,e.appendChild(t),window.addEventListener("resize",(()=>{t.width=window.innerWidth,t.height=window.innerHeight,Uo=!0})),t}(l,In.createCanvas()),w={final:!1,requesting:!0,log:[],logPointer:0,speeds:[.5,1,2,5,10,20,50,100,250,500],speedIdx:1,paused:!1,lastRealTs:0,lastGameTs:0,gameStartTs:0,dataOffset:0,dataSize:1e4};nn.onConnectionStateChange((e=>{a.setConnectionState(e)}));const b=async e=>{w.requesting=!0;const t=await nn.requestReplayData(e,w.dataOffset,w.dataSize);return w.dataOffset+=w.dataSize,w.requesting=!1,t};let x=()=>0;const C=async()=>{if("play"===o){const o=await nn.connect(n,e,t),l=ae.decodeGame(o);So.setGame(l.id,l),x=()=>O()}else{if("replay"!==o)throw"[ 2020-12-22 MODE invalid, must be play|replay ]";{const t=await b(e);if(!t.game)throw"[ 2021-05-29 no game received ]";const n=ae.decodeGame(t.game);So.setGame(n.id,n),w.requesting=!1,w.log=t.log,w.lastRealTs=O(),w.gameStartTs=parseInt(w.log[0][4],10),w.lastGameTs=w.gameStartTs,w.final=!1,w.logPointer=0,w.speeds=[.5,1,2,5,10,20,50,100,250,500],w.speedIdx=1,w.paused=!1,w.dataOffset=0,w.dataSize=1e4,x=()=>w.lastGameTs}}Uo=!0};await C();const k=So.getPieceDrawOffset(e),A=So.getPieceDrawSize(e),S=So.getPuzzleWidth(e),z=So.getPuzzleHeight(e),T=So.getTableWidth(e),P=So.getTableHeight(e),I={x:(T-S)/2,y:(P-z)/2},D={w:S,h:z},E={w:A,h:A},_=await $n.loadPuzzleBitmaps(So.getPuzzle(e)),M=new _o(v,So.getRng(e));M.init();const N=v.getContext("2d");v.classList.add("loaded");const B=Tn();B.move(-(T-v.width)/2,-(P-v.height)/2);const U=function(e,t,n){let o=[],l=!0,a=!1,i=!1,s=!1,r=!1,d=!1,c=!1,u=!1;const p=(e,t)=>{const o=n.viewportToWorld({x:e,y:t});return[o.x,o.y]},g=e=>p(e.offsetX,e.offsetY),h=()=>p(e.width/2,e.height/2),m=(e,t)=>{l&&("Shift"===t.key?u=e:"ArrowUp"===t.key||"w"===t.key||"W"===t.key?s=e:"ArrowDown"===t.key||"s"===t.key||"S"===t.key?r=e:"ArrowLeft"===t.key||"a"===t.key||"A"===t.key?a=e:"ArrowRight"===t.key||"d"===t.key||"D"===t.key?i=e:"q"===t.key?c=e:"e"===t.key&&(d=e))};let y=null;e.addEventListener("mousedown",(e=>{y=g(e),0===e.button&&f([_t,...y])})),e.addEventListener("mouseup",(e=>{y=g(e),0===e.button&&f([Mt,...y])})),e.addEventListener("mousemove",(e=>{y=g(e),f([Ot,...y])})),e.addEventListener("wheel",(e=>{if(y=g(e),n.canZoom(e.deltaY<0?"in":"out")){const t=e.deltaY<0?Nt:Bt;f([t,...y])}})),t.addEventListener("keydown",(e=>m(!0,e))),t.addEventListener("keyup",(e=>m(!1,e))),t.addEventListener("keypress",(e=>{l&&(" "===e.key&&f([$t]),"F"!==e.key&&"f"!==e.key||(No=!No,Uo=!0),"G"!==e.key&&"g"!==e.key||(Bo=!Bo,Uo=!0),"M"!==e.key&&"m"!==e.key||f([Gt]))}));const f=e=>{o.push(e)};return{addEvent:f,consumeAll:()=>{if(0===o.length)return[];const e=o.slice();return o=[],e},createKeyEvents:()=>{const e=(a?1:0)-(i?1:0),t=(s?1:0)-(r?1:0);if(0!==e||0!==t){const o=(u?24:12)*Math.sqrt(n.getCurrentZoom()),l=n.viewportDimToWorld({w:e*o,h:t*o});f([Et,l.w,l.h]),y&&(y[0]-=l.w,y[1]-=l.h)}if(d&&c);else if(d){if(n.canZoom("in")){const e=y||h();f([Nt,...e])}}else if(c&&n.canZoom("out")){const e=y||h();f([Bt,...e])}},setHotkeys:e=>{l=e}}}(v,window,B),R=So.getImageUrl(e),V=()=>{const t=So.getStartTs(e),n=So.getFinishTs(e),o=x();a.setFinished(!!n),a.setDuration((n||o)-t)};V(),a.setPiecesDone(So.getFinishedPiecesCount(e)),a.setPiecesTotal(So.getPieceCount(e));const $=x();a.setActivePlayers(So.getActivePlayers(e,$)),a.setIdlePlayers(So.getIdlePlayers(e,$));const G=!!So.getFinishTs(e);let F=G;const L=()=>F&&!G,j=()=>{const e=localStorage.getItem("sound_enabled");return null!==e&&"1"===e},W=()=>So.getPlayerBgColor(e,t)||localStorage.getItem("bg_color")||"#222222",q=()=>So.getPlayerColor(e,t)||localStorage.getItem("player_color")||"#ffffff";let H="",Y="",Q=!1;const Z=e=>{Q=e;const[t,n]=e?[H,"grab"]:[Y,"default"];v.style.cursor=`url('${t}') ${g} ${m}, ${n}`},K=e=>{H=In.colorizedCanvas(r,c,e).toDataURL(),Y=In.colorizedCanvas(d,u,e).toDataURL(),Z(Q)};K(q());const X=()=>{a.setReplaySpeed&&a.setReplaySpeed(w.speeds[w.speedIdx]),a.setReplayPaused&&a.setReplayPaused(w.paused)},J=[],ee=()=>{J.forEach((e=>{clearInterval(e)}))};let te;"play"===o?J.push(setInterval((()=>{V()}),1e3)):"replay"===o&&X(),"play"===o?nn.onServerChange((n=>{n[0],n[1],n[2];const o=n[3];for(const[l,a]of o)switch(l){case jt:{const n=ae.decodePlayer(a);n.id!==t&&(So.setPlayer(e,n.id,n),Uo=!0)}break;case Lt:{const t=ae.decodePiece(a);So.setPiece(e,t.idx,t),Uo=!0}break;case Ft:So.setPuzzleData(e,a),Uo=!0}F=!!So.getFinishTs(e)})):"replay"===o&&J.push(setInterval((()=>{const t=O();if(w.requesting)return void(w.lastRealTs=t);if(w.logPointer+1>=w.log.length)return w.lastRealTs=t,void(async e=>{const t=await b(e);w.log=w.log.slice(w.logPointer),w.logPointer=0,w.log.push(...t.log),t.log.length=w.log.length){w.final&&ee();break}const n=w.log[t],l=w.gameStartTs+n[n.length-1];if(l>o)break;const a=n.slice();if(a[0]===Pt){const t=a[1];So.addPlayer(e,t,l),Uo=!0}else if(a[0]===It){const t=So.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (update player) ]";So.addPlayer(e,t,l),Uo=!0}else if(a[0]===Dt){const t=So.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (handle input) ]";const n=a[2];So.handleInput(e,t,n,l),Uo=!0}w.logPointer=t}w.lastRealTs=t,w.lastGameTs=o,V()}),50));let ne=null;return te=(e=>{let t=!1;const n=e.fps||60,o=e.slow||1,l=e.update,a=e.render,i=window.requestAnimationFrame,s=1/n,r=o*s;let d,c=0,u=window.performance.now();const p=()=>{for(d=window.performance.now(),c+=Math.min(1,(d-u)/1e3);c>r;)c-=r,l(s);a(c/o),u=d,t||i(p)};return i(p),{stop:()=>{t=!0}}})({update:()=>{U.createKeyEvents();for(const n of U.consumeAll())if("play"===o){const o=n[0];if(o===Et){const e=n[1],t=n[2],o=B.worldDimToViewport({w:e,h:t});Uo=!0,B.move(o.w,o.h)}else if(o===Ot){if(ne&&!So.getFirstOwnedPiece(e,t)){const e={x:n[1],y:n[2]},t=B.worldToViewport(e),o=Math.round(t.x-ne.x),l=Math.round(t.y-ne.y);Uo=!0,B.move(o,l),ne=t}}else if(o===Rt)K(n[1]);else if(o===_t){const e={x:n[1],y:n[2]};ne=B.worldToViewport(e),Z(!0)}else if(o===Mt)ne=null,Z(!1);else if(o===Nt){const e={x:n[1],y:n[2]};Uo=!0,B.zoom("in",B.worldToViewport(e))}else if(o===Bt){const e={x:n[1],y:n[2]};Uo=!0,B.zoom("out",B.worldToViewport(e))}else o===$t?a.togglePreview():o===Gt&&a.toggleSoundsEnabled();const l=x();So.handleInput(e,t,n,l,(e=>{j()&&s.play()})).length>0&&(Uo=!0),nn.sendClientEvent(n)}else if("replay"===o){const e=n[0];if(e===Et){const e=n[1],t=n[2];Uo=!0,B.move(e,t)}else if(e===Ot){if(ne){const e={x:n[1],y:n[2]},t=B.worldToViewport(e),o=Math.round(t.x-ne.x),l=Math.round(t.y-ne.y);Uo=!0,B.move(o,l),ne=t}}else if(e===_t){const e={x:n[1],y:n[2]};ne=B.worldToViewport(e)}else if(e===Mt)ne=null;else if(e===Nt){const e={x:n[1],y:n[2]};Uo=!0,B.zoom("in",B.worldToViewport(e))}else if(e===Bt){const e={x:n[1],y:n[2]};Uo=!0,B.zoom("out",B.worldToViewport(e))}else e===$t&&a.togglePreview()}F=!!So.getFinishTs(e),L()&&(M.update(),Uo=!0)},render:async()=>{if(!Uo)return;const n=x();let l,i,s;window.DEBUG&&Mn(0),N.fillStyle=W(),N.fillRect(0,0,v.width,v.height),window.DEBUG&&On("clear done"),l=B.worldToViewportRaw(I),i=B.worldDimToViewportRaw(D),N.fillStyle="rgba(255, 255, 255, .3)",N.fillRect(l.x,l.y,i.w,i.h),window.DEBUG&&On("board done");const r=So.getPiecesSortedByZIndex(e);window.DEBUG&&On("get tiles done"),i=B.worldDimToViewportRaw(E);for(const e of r)(-1===e.owner?No:Bo)&&(s=_[e.idx],l=B.worldToViewportRaw({x:k+e.pos.x,y:k+e.pos.y}),N.drawImage(s,0,0,s.width,s.height,l.x,l.y,i.w,i.h));window.DEBUG&&On("tiles done");const d=[];for(const a of So.getActivePlayers(e,n))c=a,("replay"===o||c.id!==t)&&(s=await f(a),l=B.worldToViewport(a),N.drawImage(s,l.x-g,l.y-m),d.push([`${a.name} (${a.points})`,l.x,l.y+h]));var c;N.fillStyle="white",N.textAlign="center";for(const[e,t,o]of d)N.fillText(e,t,o);window.DEBUG&&On("players done"),a.setActivePlayers(So.getActivePlayers(e,n)),a.setIdlePlayers(So.getIdlePlayers(e,n)),a.setPiecesDone(So.getFinishedPiecesCount(e)),window.DEBUG&&On("HUD done"),L()&&M.render(),Uo=!1}}),{setHotkeys:e=>{U.setHotkeys(e)},onBgChange:e=>{localStorage.setItem("bg_color",e),U.addEvent([Ut,e])},onColorChange:e=>{localStorage.setItem("player_color",e),U.addEvent([Rt,e])},onNameChange:e=>{localStorage.setItem("player_name",e),U.addEvent([Vt,e])},onSoundsEnabledChange:e=>{localStorage.setItem("sound_enabled",e?"1":"0")},replayOnSpeedUp:()=>{w.speedIdx+1{w.speedIdx>=1&&(w.speedIdx--,X())},replayOnPauseToggle:()=>{w.paused=!w.paused,X()},previewImageUrl:R,player:{background:W(),color:q(),name:So.getPlayerName(e,t)||localStorage.getItem("player_name")||"anon",soundsEnabled:j()},disconnect:nn.disconnect,connect:C,unload:()=>{ee(),te&&te.stop()}}}var Vo=e({name:"game",components:{PuzzleStatus:mt,Scores:ct,SettingsOverlay:ft,PreviewOverlay:Ct,ConnectionOverlay:on,HelpOverlay:cn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:"",soundsEnabled:!1},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},onSoundsEnabledChange:e=>{},connect:()=>{},disconnect:()=>{},unload:()=>{}}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.$watch((()=>this.g.player.soundsEnabled),(e=>{this.g.onSoundsEnabledChange(e)})),this.g=await Ro(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,"play",this.$el,{setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},setConnectionState:e=>{this.connectionState=e},togglePreview:()=>{this.toggle("preview",!1)},toggleSoundsEnabled:()=>{this.g.player.soundsEnabled=!this.g.player.soundsEnabled}}))},unmounted(){this.g.unload(),this.g.disconnect()},methods:{reconnect(){this.g.connect()},toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}}});const $o={id:"game"},Go={class:"menu"},Fo={class:"tabs"},Lo=s("🧩 Puzzles");Vo.render=function(e,l,s,r,d,c){const u=a("settings-overlay"),g=a("preview-overlay"),h=a("help-overlay"),m=a("connection-overlay"),y=a("puzzle-status"),f=a("router-link"),v=a("scores");return i(),t("div",$o,[p(n(u,{onBgclick:l[1]||(l[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":l[2]||(l[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[C,"settings"===e.overlay]]),p(n(g,{onBgclick:l[3]||(l[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[C,"preview"===e.overlay]]),p(n(h,{onBgclick:l[4]||(l[4]=t=>e.toggle("help",!0))},null,512),[[C,"help"===e.overlay]]),n(m,{connectionState:e.connectionState,onReconnect:e.reconnect},null,8,["connectionState","onReconnect"]),n(y,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},null,8,["finished","duration","piecesDone","piecesTotal"]),n("div",Go,[n("div",Fo,[n(f,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[Lo])),_:1}),n("div",{class:"opener",onClick:l[5]||(l[5]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:l[6]||(l[6]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:l[7]||(l[7]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(v,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])};var jo=e({name:"replay",components:{PuzzleStatus:mt,Scores:ct,SettingsOverlay:ft,PreviewOverlay:Ct,HelpOverlay:cn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:"",soundsEnabled:!1},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},onSoundsEnabledChange:e=>{},replayOnSpeedUp:()=>{},replayOnSpeedDown:()=>{},replayOnPauseToggle:()=>{},connect:()=>{},disconnect:()=>{},unload:()=>{}},replay:{speed:1,paused:!1}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.$watch((()=>this.g.player.soundsEnabled),(e=>{this.g.onSoundsEnabledChange(e)})),this.g=await Ro(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,"replay",this.$el,{setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},togglePreview:()=>{this.toggle("preview",!1)},setConnectionState:e=>{this.connectionState=e},setReplaySpeed:e=>{this.replay.speed=e},setReplayPaused:e=>{this.replay.paused=e},toggleSoundsEnabled:()=>{this.g.player.soundsEnabled=!this.g.player.soundsEnabled}}))},unmounted(){this.g.unload(),this.g.disconnect()},methods:{toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}},computed:{replayText(){return"Replay-Speed: "+this.replay.speed+"x"+(this.replay.paused?" Paused":"")}}});const Wo={id:"replay"},qo={class:"menu"},Ho={class:"tabs"},Yo=s("🧩 Puzzles");jo.render=function(e,l,s,d,c,u){const g=a("settings-overlay"),h=a("preview-overlay"),m=a("help-overlay"),y=a("puzzle-status"),f=a("router-link"),v=a("scores");return i(),t("div",Wo,[p(n(g,{onBgclick:l[1]||(l[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":l[2]||(l[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[C,"settings"===e.overlay]]),p(n(h,{onBgclick:l[3]||(l[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[C,"preview"===e.overlay]]),p(n(m,{onBgclick:l[4]||(l[4]=t=>e.toggle("help",!0))},null,512),[[C,"help"===e.overlay]]),n(y,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},{default:o((()=>[n("div",null,[n("div",null,r(e.replayText),1),n("button",{class:"btn",onClick:l[5]||(l[5]=t=>e.g.replayOnSpeedUp())},"⏫"),n("button",{class:"btn",onClick:l[6]||(l[6]=t=>e.g.replayOnSpeedDown())},"⏬"),n("button",{class:"btn",onClick:l[7]||(l[7]=t=>e.g.replayOnPauseToggle())},"⏸️")])])),_:1},8,["finished","duration","piecesDone","piecesTotal"]),n("div",qo,[n("div",Ho,[n(f,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[Yo])),_:1}),n("div",{class:"opener",onClick:l[8]||(l[8]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:l[9]||(l[9]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:l[10]||(l[10]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(v,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])},(async()=>{const e=await fetch("/api/conf"),t=await e.json();const n=k({history:A(),routes:[{name:"index",path:"/",component:L},{name:"new-game",path:"/new-game",component:et},{name:"game",path:"/g/:id",component:Vo},{name:"replay",path:"/replay/:id",component:jo}]});n.beforeEach(((e,t)=>{t.name&&document.documentElement.classList.remove(`view-${String(t.name)}`),document.documentElement.classList.add(`view-${String(e.name)}`)}));const o=S(z);o.config.globalProperties.$config=t,o.config.globalProperties.$clientId=function(){let e=localStorage.getItem("ID");return e||(e=ae.uniqId(),localStorage.setItem("ID",e)),e}(),o.use(n),o.mount("#app")})(); diff --git a/build/public/index.html b/build/public/index.html index 2fe3679..773251f 100644 --- a/build/public/index.html +++ b/build/public/index.html @@ -4,9 +4,9 @@ 🧩 jigsaw.hyottoko.club - + - +
diff --git a/build/server/main.js b/build/server/main.js index 6a02829..9ef521f 100644 --- a/build/server/main.js +++ b/build/server/main.js @@ -3,8 +3,6 @@ import express from 'express'; import compression from 'compression'; import multer from 'multer'; import fs from 'fs'; -import readline from 'readline'; -import stream from 'stream'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import sizeOf from 'image-size'; @@ -13,29 +11,6 @@ import sharp from 'sharp'; import v8 from 'v8'; import bsqlite from 'better-sqlite3'; -var PieceEdge; -(function (PieceEdge) { - PieceEdge[PieceEdge["Flat"] = 0] = "Flat"; - PieceEdge[PieceEdge["Out"] = 1] = "Out"; - PieceEdge[PieceEdge["In"] = -1] = "In"; -})(PieceEdge || (PieceEdge = {})); -var ScoreMode; -(function (ScoreMode) { - ScoreMode[ScoreMode["FINAL"] = 0] = "FINAL"; - ScoreMode[ScoreMode["ANY"] = 1] = "ANY"; -})(ScoreMode || (ScoreMode = {})); -var ShapeMode; -(function (ShapeMode) { - ShapeMode[ShapeMode["NORMAL"] = 0] = "NORMAL"; - ShapeMode[ShapeMode["ANY"] = 1] = "ANY"; - ShapeMode[ShapeMode["FLAT"] = 2] = "FLAT"; -})(ShapeMode || (ShapeMode = {})); -var SnapMode; -(function (SnapMode) { - SnapMode[SnapMode["NORMAL"] = 0] = "NORMAL"; - SnapMode[SnapMode["REAL"] = 1] = "REAL"; -})(SnapMode || (SnapMode = {})); - class Rng { constructor(seed) { this.rand_high = seed || 0xDEADC0DE; @@ -177,9 +152,10 @@ function encodeGame(data) { data.puzzle, data.players, data.evtInfos, - data.scoreMode || ScoreMode.FINAL, - data.shapeMode || ShapeMode.ANY, - data.snapMode || SnapMode.NORMAL, + data.scoreMode, + data.shapeMode, + data.snapMode, + data.creatorUserId, ]; } function decodeGame(data) { @@ -195,6 +171,7 @@ function decodeGame(data) { scoreMode: data[6], shapeMode: data[7], snapMode: data[8], + creatorUserId: data[9], }; } function coordByPieceIdx(info, pieceIdx) { @@ -360,6 +337,13 @@ const INPUT_EV_PLAYER_NAME = 8; const INPUT_EV_MOVE = 9; const INPUT_EV_TOGGLE_PREVIEW = 10; const INPUT_EV_TOGGLE_SOUNDS = 11; +const INPUT_EV_REPLAY_TOGGLE_PAUSE = 12; +const INPUT_EV_REPLAY_SPEED_UP = 13; +const INPUT_EV_REPLAY_SPEED_DOWN = 14; +const INPUT_EV_TOGGLE_PLAYER_NAMES = 15; +const INPUT_EV_CENTER_FIT_PUZZLE = 16; +const INPUT_EV_TOGGLE_FIXED_PIECES = 17; +const INPUT_EV_TOGGLE_LOOSE_PIECES = 18; const CHANGE_DATA = 1; const CHANGE_TILE = 2; const CHANGE_PLAYER = 3; @@ -383,6 +367,13 @@ var Protocol = { INPUT_EV_PLAYER_NAME, INPUT_EV_TOGGLE_PREVIEW, INPUT_EV_TOGGLE_SOUNDS, + INPUT_EV_REPLAY_TOGGLE_PAUSE, + INPUT_EV_REPLAY_SPEED_UP, + INPUT_EV_REPLAY_SPEED_DOWN, + INPUT_EV_TOGGLE_PLAYER_NAMES, + INPUT_EV_CENTER_FIT_PUZZLE, + INPUT_EV_TOGGLE_FIXED_PIECES, + INPUT_EV_TOGGLE_LOOSE_PIECES, CHANGE_DATA, CHANGE_TILE, CHANGE_PLAYER, @@ -486,6 +477,47 @@ var Time = { durationStr, }; +var PieceEdge; +(function (PieceEdge) { + PieceEdge[PieceEdge["Flat"] = 0] = "Flat"; + PieceEdge[PieceEdge["Out"] = 1] = "Out"; + PieceEdge[PieceEdge["In"] = -1] = "In"; +})(PieceEdge || (PieceEdge = {})); +var ScoreMode; +(function (ScoreMode) { + ScoreMode[ScoreMode["FINAL"] = 0] = "FINAL"; + ScoreMode[ScoreMode["ANY"] = 1] = "ANY"; +})(ScoreMode || (ScoreMode = {})); +var ShapeMode; +(function (ShapeMode) { + ShapeMode[ShapeMode["NORMAL"] = 0] = "NORMAL"; + ShapeMode[ShapeMode["ANY"] = 1] = "ANY"; + ShapeMode[ShapeMode["FLAT"] = 2] = "FLAT"; +})(ShapeMode || (ShapeMode = {})); +var SnapMode; +(function (SnapMode) { + SnapMode[SnapMode["NORMAL"] = 0] = "NORMAL"; + SnapMode[SnapMode["REAL"] = 1] = "REAL"; +})(SnapMode || (SnapMode = {})); +const DefaultScoreMode = (v) => { + if (v === ScoreMode.FINAL || v === ScoreMode.ANY) { + return v; + } + return ScoreMode.FINAL; +}; +const DefaultShapeMode = (v) => { + if (v === ShapeMode.NORMAL || v === ShapeMode.ANY || v === ShapeMode.FLAT) { + return v; + } + return ShapeMode.NORMAL; +}; +const DefaultSnapMode = (v) => { + if (v === SnapMode.NORMAL || v === SnapMode.REAL) { + return v; + } + return SnapMode.NORMAL; +}; + const IDLE_TIMEOUT_SEC = 30; // Map const GAMES = {}; @@ -580,12 +612,16 @@ function setEvtInfo(gameId, playerId, evtInfo) { } function getAllGames() { return Object.values(GAMES).sort((a, b) => { + const finished = isFinished(a.id); // when both have same finished state, sort by started - if (isFinished(a.id) === isFinished(b.id)) { + if (finished === isFinished(b.id)) { + if (finished) { + return b.puzzle.data.finished - a.puzzle.data.finished; + } return b.puzzle.data.started - a.puzzle.data.started; } // otherwise, sort: unfinished, finished - return isFinished(a.id) ? 1 : -1; + return finished ? 1 : -1; }); } function getAllPlayers(gameId) { @@ -600,16 +636,18 @@ function getPieceCount(gameId) { return GAMES[gameId].puzzle.tiles.length; } function getImageUrl(gameId) { - return GAMES[gameId].puzzle.info.imageUrl; -} -function setImageUrl(gameId, imageUrl) { - GAMES[gameId].puzzle.info.imageUrl = imageUrl; + const imageUrl = GAMES[gameId].puzzle.info.image?.url + || GAMES[gameId].puzzle.info.imageUrl; + if (!imageUrl) { + throw new Error('[2021-07-11] no image url set'); + } + return imageUrl; } function getScoreMode(gameId) { - return GAMES[gameId].scoreMode || ScoreMode.FINAL; + return GAMES[gameId].scoreMode; } function getSnapMode(gameId) { - return GAMES[gameId].snapMode || SnapMode.NORMAL; + return GAMES[gameId].snapMode; } function isFinished(gameId) { return getFinishedPiecesCount(gameId) === getPieceCount(gameId); @@ -1169,6 +1207,12 @@ function handleInput$1(gameId, playerId, input, ts, onSnap) { changePlayer(gameId, playerId, { d, ts }); _playerChange(); } + if (snapped && getSnapMode(gameId) === SnapMode.REAL) { + if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) { + changeData(gameId, { finished: ts }); + _dataChange(); + } + } if (snapped && onSnap) { onSnap(playerId); } @@ -1211,7 +1255,6 @@ var GameCommon = { getFinishedPiecesCount, getPieceCount, getImageUrl, - setImageUrl, get: get$1, getAllGames, getPlayerBgColor, @@ -1249,6 +1292,7 @@ const PUBLIC_DIR = `${BASE_DIR}/build/public/`; const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`; const DB_FILE = `${BASE_DIR}/data/db.sqlite`; +const LINES_PER_LOG_FILE = 10000; const POST_GAME_LOG_DURATION = 5 * Time.MIN; const shouldLog = (finishTs, currentTs) => { // when not finished yet, always log @@ -1260,55 +1304,65 @@ const shouldLog = (finishTs, currentTs) => { const timeSinceGameEnd = currentTs - finishTs; return timeSinceGameEnd <= POST_GAME_LOG_DURATION; }; -const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`; -const create = (gameId) => { - const file = filename(gameId); - if (!fs.existsSync(file)) { - fs.appendFileSync(file, ''); +const filename = (gameId, offset) => `${DATA_DIR}/log_${gameId}-${offset}.log`; +const idxname = (gameId) => `${DATA_DIR}/log_${gameId}.idx.log`; +const create = (gameId, ts) => { + const idxfile = idxname(gameId); + if (!fs.existsSync(idxfile)) { + fs.appendFileSync(idxfile, JSON.stringify({ + gameId: gameId, + total: 0, + lastTs: ts, + currentFile: '', + perFile: LINES_PER_LOG_FILE, + })); } }; const exists = (gameId) => { - const file = filename(gameId); - return fs.existsSync(file); + const idxfile = idxname(gameId); + return fs.existsSync(idxfile); }; -const _log = (gameId, ...args) => { - const file = filename(gameId); - if (!fs.existsSync(file)) { +const _log = (gameId, type, ...args) => { + const idxfile = idxname(gameId); + if (!fs.existsSync(idxfile)) { return; } - const str = JSON.stringify(args); - fs.appendFileSync(file, str + "\n"); + const idxObj = JSON.parse(fs.readFileSync(idxfile, 'utf-8')); + if (idxObj.total % idxObj.perFile === 0) { + idxObj.currentFile = filename(gameId, idxObj.total); + } + const tsIdx = type === Protocol.LOG_HEADER ? 3 : (args.length - 1); + const ts = args[tsIdx]; + if (type !== Protocol.LOG_HEADER) { + // for everything but header save the diff to last log entry + args[tsIdx] = ts - idxObj.lastTs; + } + const line = JSON.stringify([type, ...args]).slice(1, -1); + fs.appendFileSync(idxObj.currentFile, line + "\n"); + idxObj.total++; + idxObj.lastTs = ts; + fs.writeFileSync(idxfile, JSON.stringify(idxObj)); }; -const get = async (gameId, offset = 0, size = 10000) => { - const file = filename(gameId); +const get = (gameId, offset = 0) => { + const idxfile = idxname(gameId); + if (!fs.existsSync(idxfile)) { + return []; + } + const file = filename(gameId, offset); if (!fs.existsSync(file)) { return []; } - return new Promise((resolve) => { - const instream = fs.createReadStream(file); - const outstream = new stream.Writable(); - const rl = readline.createInterface(instream, outstream); - const lines = []; - let i = -1; - rl.on('line', (line) => { - if (!line) { - // skip empty - return; - } - i++; - if (offset > i) { - return; - } - if (offset + size <= i) { - rl.close(); - return; - } - lines.push(JSON.parse(line)); - }); - rl.on('close', () => { - resolve(lines); - }); + const lines = fs.readFileSync(file, 'utf-8').split("\n"); + const log = lines.filter(line => !!line).map(line => { + return JSON.parse(`[${line}]`); }); + if (offset === 0 && log.length > 0) { + log[0][5] = DefaultScoreMode(log[0][5]); + log[0][6] = DefaultShapeMode(log[0][6]); + log[0][7] = DefaultSnapMode(log[0][7]); + log[0][8] = log[0][8] || null; + } + return log; }; var GameLog = { shouldLog, @@ -1316,6 +1370,8 @@ var GameLog = { exists, log: _log, get, + filename, + idxname, }; const log$4 = logger('Images.ts'); @@ -1390,12 +1446,14 @@ const imageFromDb = (db, imageId) => { const i = db.get('images', { id: imageId }); return { id: i.id, + uploaderUserId: i.uploader_user_id, filename: i.filename, - file: `${UPLOAD_DIR}/${i.filename}`, url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, title: i.title, tags: getTags(db, i.id), created: i.created * 1000, + width: i.width, + height: i.height, }; }; const allImagesFromDb = (db, tagSlugs, orderBy) => { @@ -1427,35 +1485,42 @@ inner join images i on i.id = ixc.image_id ${where.sql}; const images = db.getMany('images', wheresRaw, orderByMap[orderBy]); return images.map(i => ({ id: i.id, + uploaderUserId: i.uploader_user_id, filename: i.filename, - file: `${UPLOAD_DIR}/${i.filename}`, url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, title: i.title, tags: getTags(db, i.id), created: i.created * 1000, + width: i.width, + height: i.height, })); }; +/** + * @deprecated old function, now database is used + */ const allImagesFromDisk = (tags, sort) => { let images = fs.readdirSync(UPLOAD_DIR) .filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/)) .map(f => ({ id: 0, + uploaderUserId: null, filename: f, - file: `${UPLOAD_DIR}/${f}`, url: `${UPLOAD_URL}/${encodeURIComponent(f)}`, title: f.replace(/\.[a-z]+$/, ''), tags: [], created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(), + width: 0, + height: 0, // may have to fill when the function is used again })); switch (sort) { case 'alpha_asc': images = images.sort((a, b) => { - return a.file > b.file ? 1 : -1; + return a.filename > b.filename ? 1 : -1; }); break; case 'alpha_desc': images = images.sort((a, b) => { - return a.file < b.file ? 1 : -1; + return a.filename < b.filename ? 1 : -1; }); break; case 'date_asc': @@ -1501,7 +1566,7 @@ var Images = { // final resized version of the puzzle image const TILE_SIZE = 64; async function createPuzzle(rng, targetTiles, image, ts, shapeMode) { - const imagePath = image.file; + const imagePath = `${UPLOAD_DIR}/${image.filename}`; const imageUrl = image.url; // determine puzzle information from the image dimensions const dim = await Images.getDimensions(imagePath); @@ -1600,6 +1665,7 @@ async function createPuzzle(rng, targetTiles, image, ts, shapeMode) { // information that was used to create the puzzle targetTiles: targetTiles, imageUrl, + image: image, width: info.width, height: info.height, tileSize: info.tileSize, @@ -1690,7 +1756,63 @@ function setDirty(gameId) { function setClean(gameId) { delete dirtyGames[gameId]; } -function loadGames() { +function loadGamesFromDb(db) { + const gameRows = db.getMany('games'); + for (const gameRow of gameRows) { + loadGameFromDb(db, gameRow.id); + } +} +function loadGameFromDb(db, gameId) { + const gameRow = db.get('games', { id: gameId }); + let game; + try { + game = JSON.parse(gameRow.data); + } + catch { + log$3.log(`[ERR] unable to load game from db ${gameId}`); + } + if (typeof game.puzzle.data.started === 'undefined') { + game.puzzle.data.started = gameRow.created; + } + if (typeof game.puzzle.data.finished === 'undefined') { + game.puzzle.data.finished = gameRow.finished; + } + if (!Array.isArray(game.players)) { + game.players = Object.values(game.players); + } + const gameObject = storeDataToGame(game, game.creator_user_id); + GameCommon.setGame(gameObject.id, gameObject); +} +function persistGamesToDb(db) { + for (const gameId of Object.keys(dirtyGames)) { + persistGameToDb(db, gameId); + } +} +function persistGameToDb(db, gameId) { + const game = GameCommon.get(gameId); + if (!game) { + log$3.error(`[ERROR] unable to persist non existing game ${gameId}`); + return; + } + if (game.id in dirtyGames) { + setClean(game.id); + } + db.upsert('games', { + id: game.id, + creator_user_id: game.creatorUserId, + image_id: game.puzzle.info.image?.id, + created: game.puzzle.data.started, + finished: game.puzzle.data.finished, + data: gameToStoreData(game) + }, { + id: game.id, + }); + log$3.info(`[INFO] persisted game ${game.id}`); +} +/** + * @deprecated + */ +function loadGamesFromDisk() { const files = fs.readdirSync(DATA_DIR); for (const f of files) { const m = f.match(/^([a-z0-9]+)\.json$/); @@ -1698,10 +1820,13 @@ function loadGames() { continue; } const gameId = m[1]; - loadGame(gameId); + loadGameFromDisk(gameId); } } -function loadGame(gameId) { +/** + * @deprecated + */ +function loadGameFromDisk(gameId) { const file = `${DATA_DIR}/${gameId}.json`; const contents = fs.readFileSync(file, 'utf-8'); let game; @@ -1723,27 +1848,21 @@ function loadGame(gameId) { if (!Array.isArray(game.players)) { game.players = Object.values(game.players); } - const gameObject = { - id: game.id, - rng: { - type: game.rng ? game.rng.type : '_fake_', - obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0), - }, - puzzle: game.puzzle, - players: game.players, - evtInfos: {}, - scoreMode: game.scoreMode || ScoreMode.FINAL, - shapeMode: game.shapeMode || ShapeMode.ANY, - snapMode: game.snapMode || SnapMode.NORMAL, - }; + const gameObject = storeDataToGame(game, null); GameCommon.setGame(gameObject.id, gameObject); } -function persistGames() { +/** + * @deprecated + */ +function persistGamesToDisk() { for (const gameId of Object.keys(dirtyGames)) { - persistGame(gameId); + persistGameToDisk(gameId); } } -function persistGame(gameId) { +/** + * @deprecated + */ +function persistGameToDisk(gameId) { const game = GameCommon.get(gameId); if (!game) { log$3.error(`[ERROR] unable to persist non existing game ${gameId}`); @@ -1752,7 +1871,27 @@ function persistGame(gameId) { if (game.id in dirtyGames) { setClean(game.id); } - fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({ + fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, gameToStoreData(game)); + log$3.info(`[INFO] persisted game ${game.id}`); +} +function storeDataToGame(storeData, creatorUserId) { + return { + id: storeData.id, + creatorUserId, + rng: { + type: storeData.rng ? storeData.rng.type : '_fake_', + obj: storeData.rng ? Rng.unserialize(storeData.rng.obj) : new Rng(0), + }, + puzzle: storeData.puzzle, + players: storeData.players, + evtInfos: {}, + scoreMode: DefaultScoreMode(storeData.scoreMode), + shapeMode: DefaultShapeMode(storeData.shapeMode), + snapMode: DefaultSnapMode(storeData.snapMode), + }; +} +function gameToStoreData(game) { + return JSON.stringify({ id: game.id, rng: { type: game.rng.type, @@ -1763,22 +1902,27 @@ function persistGame(gameId) { scoreMode: game.scoreMode, shapeMode: game.shapeMode, snapMode: game.snapMode, - })); - log$3.info(`[INFO] persisted game ${game.id}`); + }); } var GameStorage = { - loadGames, - loadGame, - persistGames, - persistGame, + // disk functions are deprecated + loadGamesFromDisk, + loadGameFromDisk, + persistGamesToDisk, + persistGameToDisk, + loadGamesFromDb, + loadGameFromDb, + persistGamesToDb, + persistGameToDb, setDirty, }; -async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode) { +async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId) { const seed = Util.hash(gameId + ' ' + ts); const rng = new Rng(seed); return { id: gameId, + creatorUserId, rng: { type: 'Rng', obj: rng }, puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode), players: [], @@ -1788,22 +1932,21 @@ async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shape snapMode, }; } -async function createGame(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode) { - const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode); - GameLog.create(gameId); - GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode, shapeMode, snapMode); +async function createGame(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId) { + const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId); + GameLog.create(gameId, ts); + GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode, shapeMode, snapMode, gameObject.creatorUserId); GameCommon.setGame(gameObject.id, gameObject); GameStorage.setDirty(gameId); } function addPlayer(gameId, playerId, ts) { if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) { const idx = GameCommon.getPlayerIndexById(gameId, playerId); - const diff = ts - GameCommon.getStartTs(gameId); if (idx === -1) { - GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, diff); + GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, ts); } else { - GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, diff); + GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, ts); } } GameCommon.addPlayer(gameId, playerId, ts); @@ -1812,8 +1955,7 @@ function addPlayer(gameId, playerId, ts) { function handleInput(gameId, playerId, input, ts) { if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) { const idx = GameCommon.getPlayerIndexById(gameId, playerId); - const diff = ts - GameCommon.getStartTs(gameId); - GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff); + GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, ts); } const ret = GameCommon.handleInput(gameId, playerId, input, ts); GameStorage.setDirty(gameId); @@ -2039,6 +2181,13 @@ const storage = multer.diskStorage({ } }); const upload = multer({ storage }).single('file'); +app.get('/api/me', (req, res) => { + let user = getUser(db, req); + res.send({ + id: user ? user.id : null, + created: user ? user.created : null, + }); +}); app.get('/api/conf', (req, res) => { res.send({ WS_ADDRESS: config.ws.connectstring, @@ -2061,11 +2210,12 @@ app.get('/api/replay-data', async (req, res) => { res.status(404).send({ reason: 'no log found' }); return; } - const log = await GameLog.get(gameId, offset, size); + const log = GameLog.get(gameId, offset); let game = null; if (offset === 0) { // also need the game - game = await Game.createGameObject(gameId, log[0][2], log[0][3], log[0][4], log[0][5] || ScoreMode.FINAL, log[0][6] || ShapeMode.NORMAL, log[0][7] || SnapMode.NORMAL); + game = await Game.createGameObject(gameId, log[0][2], log[0][3], // must be ImageInfo + log[0][4], log[0][5], log[0][6], log[0][7], log[0][8]); } res.send({ log, game: game ? Util.encodeGame(game) : null }); }); @@ -2096,6 +2246,28 @@ app.get('/api/index-data', (req, res) => { gamesFinished: games.filter(g => !!g.finished), }); }); +const getOrCreateUser = (db, req) => { + let user = getUser(db, req); + if (!user) { + db.insert('users', { + 'client_id': req.headers['client-id'], + 'client_secret': req.headers['client-secret'], + 'created': Time.timestamp(), + }); + user = getUser(db, req); + } + return user; +}; +const getUser = (db, req) => { + let user = db.get('users', { + 'client_id': req.headers['client-id'], + 'client_secret': req.headers['client-secret'], + }); + if (user) { + user.id = parseInt(user.id, 10); + } + return user; +}; const setImageTags = (db, imageId, tags) => { tags.forEach((tag) => { const slug = Util.slug(tag); @@ -2109,7 +2281,17 @@ const setImageTags = (db, imageId, tags) => { }); }; app.post('/api/save-image', express.json(), (req, res) => { + let user = getUser(db, req); + if (!user || !user.id) { + res.status(403).send({ ok: false, error: 'forbidden' }); + return; + } const data = req.body; + let image = db.get('images', { id: data.id }); + if (parseInt(image.uploader_user_id, 10) !== user.id) { + res.status(403).send({ ok: false, error: 'forbidden' }); + return; + } db.update('images', { title: data.title, }, { @@ -2134,11 +2316,16 @@ app.post('/api/upload', (req, res) => { log.log(err); res.status(400).send("Something went wrong!"); } + const user = getOrCreateUser(db, req); + const dim = await Images.getDimensions(`${UPLOAD_DIR}/${req.file.filename}`); const imageId = db.insert('images', { + uploader_user_id: user.id, filename: req.file.filename, filename_original: req.file.originalname, title: req.body.title || '', created: Time.timestamp(), + width: dim.w, + height: dim.h, }); if (req.body.tags) { const tags = req.body.tags.split(',').filter((tag) => !!tag); @@ -2148,12 +2335,17 @@ app.post('/api/upload', (req, res) => { }); }); app.post('/api/newgame', express.json(), async (req, res) => { + let user = getOrCreateUser(db, req); + if (!user || !user.id) { + res.status(403).send({ ok: false, error: 'forbidden' }); + return; + } const gameSettings = req.body; log.log(gameSettings); const gameId = Util.uniqId(); if (!GameCommon.exists(gameId)) { const ts = Time.timestamp(); - await Game.createGame(gameId, gameSettings.tiles, gameSettings.image, ts, gameSettings.scoreMode, gameSettings.shapeMode, gameSettings.snapMode); + await Game.createGame(gameId, gameSettings.tiles, gameSettings.image, ts, gameSettings.scoreMode, gameSettings.shapeMode, gameSettings.snapMode, user.id); } res.send({ id: gameId }); }); @@ -2235,7 +2427,7 @@ wss.on('message', async ({ socket, data }) => { log.error(e); } }); -GameStorage.loadGames(); +GameStorage.loadGamesFromDb(db); const server = app.listen(port, hostname, () => log.log(`server running on http://${hostname}:${port}`)); wss.listen(); const memoryUsageHuman = () => { @@ -2249,7 +2441,7 @@ memoryUsageHuman(); // persist games in fixed interval const persistInterval = setInterval(() => { log.log('Persisting games...'); - GameStorage.persistGames(); + GameStorage.persistGamesToDb(db); memoryUsageHuman(); }, config.persistence.interval); const gracefulShutdown = (signal) => { @@ -2257,7 +2449,7 @@ const gracefulShutdown = (signal) => { log.log('clearing persist interval...'); clearInterval(persistInterval); log.log('persisting games...'); - GameStorage.persistGames(); + GameStorage.persistGamesToDb(db); log.log('shutting down webserver...'); server.close(); log.log('shutting down websocketserver...'); diff --git a/rollup.server.config.js b/rollup.server.config.js index 091ff79..484815c 100644 --- a/rollup.server.config.js +++ b/rollup.server.config.js @@ -16,9 +16,7 @@ export default { "image-size", "multer", "path", - "readline", "sharp", - "stream", "url", "v8", "ws", diff --git a/scripts/fix_games_image_info.ts b/scripts/fix_games_image_info.ts new file mode 100644 index 0000000..49ac4ed --- /dev/null +++ b/scripts/fix_games_image_info.ts @@ -0,0 +1,90 @@ +import GameCommon from '../src/common/GameCommon' +import GameLog from '../src/server/GameLog' +import { Game } from '../src/common/Types' +import { logger } from '../src/common/Util' +import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs' +import Db from '../src/server/Db' +import GameStorage from '../src/server/GameStorage' +import fs from 'fs' + +const log = logger('fix_games_image_info.ts') + +import Images from '../src/server/Images' + +console.log(DB_FILE) + +const db = new Db(DB_FILE, DB_PATCHES_DIR) +db.patch(true) + +// ;(async () => { +// let images = db.getMany('images') +// for (let image of images) { +// console.log(image.filename) +// let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`) +// console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)) +// image.width = dim.w +// image.height = dim.h +// db.upsert('images', image, { id: image.id }) +// } +// })() + +function fixOne(gameId: string) { + let g = GameCommon.get(gameId) + if (!g) { + return + } + + if (!g.puzzle.info.image && g.puzzle.info.imageUrl) { + log.log('game id: ', gameId) + const parts = g.puzzle.info.imageUrl.split('/') + const fileName = parts[parts.length - 1] + const imageRow = db.get('images', {filename: fileName}) + if (!imageRow) { + return + } + + g.puzzle.info.image = Images.imageFromDb(db, imageRow.id) + + log.log(g.puzzle.info.image.title, imageRow.id) + + GameStorage.persistGameToDb(db, gameId) + } else if (g.puzzle.info.image?.id) { + const imageId = g.puzzle.info.image.id + + g.puzzle.info.image = Images.imageFromDb(db, imageId) + + log.log(g.puzzle.info.image.title, imageId) + + GameStorage.persistGameToDb(db, gameId) + } + + // fix log + const file = GameLog.filename(gameId, 0) + if (!fs.existsSync(file)) { + return + } + + const lines = fs.readFileSync(file, 'utf-8').split("\n") + const l = lines.filter(line => !!line).map(line => { + return JSON.parse(`[${line}]`) + }) + if (l && l[0] && !l[0][3].id) { + log.log(l[0][3]) + l[0][3] = g.puzzle.info.image + const newlines = l.map(ll => { + return JSON.stringify(ll).slice(1, -1) + }).join("\n") + "\n" + console.log(g.puzzle.info.image) + // process.exit(0) + fs.writeFileSync(file, newlines) + } +} + +function fix() { + GameStorage.loadGamesFromDisk() + GameCommon.getAllGames().forEach((game: Game) => { + fixOne(game.id) + }) +} + +fix() diff --git a/scripts/fix_image.ts b/scripts/fix_image.ts deleted file mode 100644 index 366ed1f..0000000 --- a/scripts/fix_image.ts +++ /dev/null @@ -1,23 +0,0 @@ -import GameCommon from '../src/common/GameCommon' -import { logger } from '../src/common/Util' -import GameStorage from '../src/server/GameStorage' - -const log = logger('fix_image.js') - -function fix(gameId) { - GameStorage.loadGame(gameId) - let changed = false - - let imgUrl = GameCommon.getImageUrl(gameId) - if (imgUrl.match(/^\/example-images\//)) { - log.log(`found bad imgUrl: ${imgUrl}`) - imgUrl = imgUrl.replace(/^\/example-images\//, '/uploads/') - GameCommon.setImageUrl(gameId, imgUrl) - changed = true - } - if (changed) { - GameStorage.persistGame(gameId) - } -} - -fix(process.argv[2]) diff --git a/scripts/fix_tiles.ts b/scripts/fix_tiles.ts index 5eb7ee9..a45b19b 100644 --- a/scripts/fix_tiles.ts +++ b/scripts/fix_tiles.ts @@ -1,11 +1,16 @@ import GameCommon from '../src/common/GameCommon' import { logger } from '../src/common/Util' +import Db from '../src/server/Db' +import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs' import GameStorage from '../src/server/GameStorage' const log = logger('fix_tiles.js') +const db = new Db(DB_FILE, DB_PATCHES_DIR) +db.patch(true) + function fix_tiles(gameId) { - GameStorage.loadGame(gameId) + GameStorage.loadGameFromDb(db, gameId) let changed = false const tiles = GameCommon.getPiecesSortedByZIndex(gameId) for (let tile of tiles) { @@ -27,7 +32,7 @@ function fix_tiles(gameId) { } } if (changed) { - GameStorage.persistGame(gameId) + GameStorage.persistGameToDb(db, gameId) } } diff --git a/scripts/import_games.ts b/scripts/import_games.ts new file mode 100644 index 0000000..f2e5cfa --- /dev/null +++ b/scripts/import_games.ts @@ -0,0 +1,27 @@ +import GameCommon from '../src/common/GameCommon' +import { Game } from '../src/common/Types' +import { logger } from '../src/common/Util' +import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs' +import Db from '../src/server/Db' +import GameStorage from '../src/server/GameStorage' + +const log = logger('import_games.ts') + +console.log(DB_FILE) + +const db = new Db(DB_FILE, DB_PATCHES_DIR) +db.patch(true) + +function run() { + GameStorage.loadGamesFromDisk() + GameCommon.getAllGames().forEach((game: Game) => { + if (!game.puzzle.info.image?.id) { + log.error(game.id + " has no image") + log.error(game.puzzle.info.image) + return + } + GameStorage.persistGameToDb(db, game.id) + }) +} + +run() diff --git a/scripts/import_image_sizes.ts b/scripts/import_image_sizes.ts new file mode 100644 index 0000000..9c9365c --- /dev/null +++ b/scripts/import_image_sizes.ts @@ -0,0 +1,18 @@ +import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs' +import Db from '../src/server/Db' +import Images from '../src/server/Images' + +const db = new Db(DB_FILE, DB_PATCHES_DIR) +db.patch(true) + +;(async () => { + let images = db.getMany('images') + for (let image of images) { + console.log(image.filename) + let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`) + console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)) + image.width = dim.w + image.height = dim.h + db.upsert('images', image, { id: image.id }) + } +})() diff --git a/scripts/rewrite_logs.ts b/scripts/rewrite_logs.ts deleted file mode 100644 index 0ac3c99..0000000 --- a/scripts/rewrite_logs.ts +++ /dev/null @@ -1,82 +0,0 @@ -import fs from 'fs' -import Protocol from '../src/common/Protocol' -import { logger } from '../src/common/Util' -import { DATA_DIR } from '../src/server/Dirs' - -const log = logger('rewrite_logs') - -const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log` - -const rewrite = (gameId) => { - const file = filename(gameId) - log.log(file) - if (!fs.existsSync(file)) { - return [] - } - let playerIds = []; - let startTs = null - const lines = fs.readFileSync(file, 'utf-8').split("\n") - const linesNew = lines.filter(line => !!line).map((line) => { - const json = JSON.parse(line) - const m = { - createGame: Protocol.LOG_HEADER, - addPlayer: Protocol.LOG_ADD_PLAYER, - handleInput: Protocol.LOG_HANDLE_INPUT, - } - const action = json[0] - if (action in m) { - json[0] = m[action] - if (json[0] === Protocol.LOG_HANDLE_INPUT) { - const inputm = { - down: Protocol.INPUT_EV_MOUSE_DOWN, - up: Protocol.INPUT_EV_MOUSE_UP, - move: Protocol.INPUT_EV_MOUSE_MOVE, - zoomin: Protocol.INPUT_EV_ZOOM_IN, - zoomout: Protocol.INPUT_EV_ZOOM_OUT, - bg_color: Protocol.INPUT_EV_BG_COLOR, - player_color: Protocol.INPUT_EV_PLAYER_COLOR, - player_name: Protocol.INPUT_EV_PLAYER_NAME, - } - const inputa = json[2][0] - if (inputa in inputm) { - json[2][0] = inputm[inputa] - } else { - throw '[ invalid input log line: "' + line + '" ]' - } - } - } else { - throw '[ invalid general log line: "' + line + '" ]' - } - - if (json[0] === Protocol.LOG_ADD_PLAYER) { - if (playerIds.indexOf(json[1]) === -1) { - playerIds.push(json[1]) - } else { - json[0] = Protocol.LOG_UPDATE_PLAYER - json[1] = playerIds.indexOf(json[1]) - } - } - - if (json[0] === Protocol.LOG_HANDLE_INPUT) { - json[1] = playerIds.indexOf(json[1]) - if (json[1] === -1) { - throw '[ invalid player ... "' + line + '" ]' - } - } - - if (json[0] === Protocol.LOG_HEADER) { - startTs = json[json.length - 1] - json[4] = json[3] - json[3] = json[2] - json[2] = json[1] - json[1] = 1 - } else { - json[json.length - 1] = json[json.length - 1] - startTs - } - return JSON.stringify(json) - }) - - fs.writeFileSync(file, linesNew.join("\n") + "\n") -} - -rewrite(process.argv[2]) diff --git a/scripts/server b/scripts/server index 182e3a4..4d1a1c9 100755 --- a/scripts/server +++ b/scripts/server @@ -1,4 +1,4 @@ #!/bin/sh # server for built files -nodemon --max-old-space-size=64 -e js build/server/main.js -c config.json +nodemon --watch build --max-old-space-size=64 -e js build/server/main.js -c config.json diff --git a/scripts/split_logs.ts b/scripts/split_logs.ts new file mode 100644 index 0000000..5497096 --- /dev/null +++ b/scripts/split_logs.ts @@ -0,0 +1,73 @@ +import fs from 'fs' +import { logger } from '../src/common/Util' +import { DATA_DIR } from '../src/server/Dirs' +import { filename } from '../src/server/GameLog' + +const log = logger('rewrite_logs') + +interface IdxOld { + total: number + currentFile: string + perFile: number +} + +interface Idx { + gameId: string + total: number + lastTs: number + currentFile: string + perFile: number +} +const doit = (idxfile: string): void => { + const gameId: string = (idxfile.match(/^log_([a-z0-9]+)\.idx\.log$/) as any[])[1] + const idxOld: IdxOld = JSON.parse(fs.readFileSync(DATA_DIR + '/' + idxfile, 'utf-8')) + + let currentFile = filename(gameId, 0) + const idxNew: Idx = { + gameId: gameId, + total: 0, + lastTs: 0, + currentFile: currentFile, + perFile: idxOld.perFile + } + + let firstTs = 0 + while (fs.existsSync(currentFile)) { + idxNew.currentFile = currentFile + const log = fs.readFileSync(currentFile, 'utf-8').split("\n") + const newLines = [] + const lines = log.filter(line => !!line).map(line => { + return JSON.parse(line) + }) + for (const l of lines) { + if (idxNew.total === 0) { + firstTs = l[4] + idxNew.lastTs = l[4] + newLines.push(JSON.stringify(l).slice(1, -1)) + } else { + const ts = firstTs + l[l.length - 1] + const diff = ts - idxNew.lastTs + idxNew.lastTs = ts + const newL = l.slice(0, -1) + newL.push(diff) + newLines.push(JSON.stringify(newL).slice(1, -1)) + } + idxNew.total++ + } + fs.writeFileSync(idxNew.currentFile, newLines.join("\n") + "\n") + currentFile = filename(gameId, idxNew.total) + } + + fs.writeFileSync(DATA_DIR + '/' + idxfile, JSON.stringify(idxNew)) + console.log('done: ' + gameId) +} + +let indexfiles = fs.readdirSync(DATA_DIR) + .filter(f => f.toLowerCase().match(/^log_[a-z0-9]+\.idx\.log$/)) + + +;(async () => { + for (const file of indexfiles) { + await doit(file) + } +})() diff --git a/scripts/ts b/scripts/ts index 176a61c..8c28ec5 100755 --- a/scripts/ts +++ b/scripts/ts @@ -1,3 +1,3 @@ #!/bin/sh -e -node --experimental-specifier-resolution=node --loader ts-node/esm $@ +node --max-old-space-size=256 --experimental-specifier-resolution=node --loader ts-node/esm $@ diff --git a/src/common/GameCommon.ts b/src/common/GameCommon.ts index f1aee84..8b7fde7 100644 --- a/src/common/GameCommon.ts +++ b/src/common/GameCommon.ts @@ -138,12 +138,16 @@ function setEvtInfo( function getAllGames(): Array { return Object.values(GAMES).sort((a: Game, b: Game) => { + const finished = isFinished(a.id) // when both have same finished state, sort by started - if (isFinished(a.id) === isFinished(b.id)) { + if (finished === isFinished(b.id)) { + if (finished) { + return b.puzzle.data.finished - a.puzzle.data.finished + } return b.puzzle.data.started - a.puzzle.data.started } // otherwise, sort: unfinished, finished - return isFinished(a.id) ? 1 : -1 + return finished ? 1 : -1 }) } @@ -162,19 +166,20 @@ function getPieceCount(gameId: string): number { } function getImageUrl(gameId: string): string { - return GAMES[gameId].puzzle.info.imageUrl -} - -function setImageUrl(gameId: string, imageUrl: string): void { - GAMES[gameId].puzzle.info.imageUrl = imageUrl + const imageUrl = GAMES[gameId].puzzle.info.image?.url + || GAMES[gameId].puzzle.info.imageUrl + if (!imageUrl) { + throw new Error('[2021-07-11] no image url set') + } + return imageUrl } function getScoreMode(gameId: string): ScoreMode { - return GAMES[gameId].scoreMode || ScoreMode.FINAL + return GAMES[gameId].scoreMode } function getSnapMode(gameId: string): SnapMode { - return GAMES[gameId].snapMode || SnapMode.NORMAL + return GAMES[gameId].snapMode } function isFinished(gameId: string): boolean { @@ -848,6 +853,13 @@ function handleInput( changePlayer(gameId, playerId, { d, ts }) _playerChange() } + + if (snapped && getSnapMode(gameId) === SnapMode.REAL) { + if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) { + changeData(gameId, { finished: ts }) + _dataChange() + } + } if (snapped && onSnap) { onSnap(playerId) } @@ -888,7 +900,6 @@ export default { getFinishedPiecesCount, getPieceCount, getImageUrl, - setImageUrl, get, getAllGames, getPlayerBgColor, diff --git a/src/common/Protocol.ts b/src/common/Protocol.ts index d1dd698..1f868c1 100644 --- a/src/common/Protocol.ts +++ b/src/common/Protocol.ts @@ -43,11 +43,6 @@ const EV_SERVER_INIT = 4 const EV_CLIENT_EVENT = 2 const EV_CLIENT_INIT = 3 -const EV_CLIENT_INIT_REPLAY = 5 -const EV_SERVER_INIT_REPLAY = 6 -const EV_CLIENT_REPLAY_EVENT = 7 -const EV_SERVER_REPLAY_EVENT = 8 - const LOG_HEADER = 1 const LOG_ADD_PLAYER = 2 const LOG_UPDATE_PLAYER = 4 @@ -65,6 +60,16 @@ const INPUT_EV_MOVE = 9 const INPUT_EV_TOGGLE_PREVIEW = 10 const INPUT_EV_TOGGLE_SOUNDS = 11 +const INPUT_EV_REPLAY_TOGGLE_PAUSE = 12 +const INPUT_EV_REPLAY_SPEED_UP = 13 +const INPUT_EV_REPLAY_SPEED_DOWN = 14 + +const INPUT_EV_TOGGLE_PLAYER_NAMES = 15 +const INPUT_EV_CENTER_FIT_PUZZLE = 16 + +const INPUT_EV_TOGGLE_FIXED_PIECES = 17 +const INPUT_EV_TOGGLE_LOOSE_PIECES = 18 + const CHANGE_DATA = 1 const CHANGE_TILE = 2 const CHANGE_PLAYER = 3 @@ -75,11 +80,6 @@ export default { EV_CLIENT_EVENT, EV_CLIENT_INIT, - EV_CLIENT_INIT_REPLAY, - EV_SERVER_INIT_REPLAY, - EV_CLIENT_REPLAY_EVENT, - EV_SERVER_REPLAY_EVENT, - LOG_HEADER, LOG_ADD_PLAYER, LOG_UPDATE_PLAYER, @@ -100,6 +100,16 @@ export default { INPUT_EV_TOGGLE_PREVIEW, INPUT_EV_TOGGLE_SOUNDS, + INPUT_EV_REPLAY_TOGGLE_PAUSE, + INPUT_EV_REPLAY_SPEED_UP, + INPUT_EV_REPLAY_SPEED_DOWN, + + INPUT_EV_TOGGLE_PLAYER_NAMES, + INPUT_EV_CENTER_FIT_PUZZLE, + + INPUT_EV_TOGGLE_FIXED_PIECES, + INPUT_EV_TOGGLE_LOOSE_PIECES, + CHANGE_DATA, CHANGE_TILE, CHANGE_PLAYER, diff --git a/src/common/Types.ts b/src/common/Types.ts index 90de5b3..ccbb318 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -51,6 +51,7 @@ export type EncodedGame = FixedLengthArray<[ ScoreMode, ShapeMode, SnapMode, + number|null, ]> export interface ReplayData { @@ -72,12 +73,13 @@ interface GameRng { export interface Game { id: string + creatorUserId: number|null players: Array puzzle: Puzzle evtInfos: Record - scoreMode?: ScoreMode - shapeMode?: ShapeMode - snapMode?: SnapMode + scoreMode: ScoreMode + shapeMode: ShapeMode + snapMode: SnapMode rng: GameRng } @@ -93,7 +95,7 @@ export interface Image { export interface GameSettings { tiles: number - image: Image + image: ImageInfo scoreMode: ScoreMode shapeMode: ShapeMode snapMode: SnapMode @@ -152,10 +154,24 @@ export interface PieceChange { group?: number } +export interface ImageInfo +{ + id: number + uploaderUserId: number|null + filename: string + url: string + title: string + tags: Tag[] + created: Timestamp + width: number + height: number +} + export interface PuzzleInfo { table: PuzzleTable - targetTiles: number, - imageUrl: string + targetTiles: number + imageUrl?: string // deprecated, use image.url instead + image?: ImageInfo width: number height: number @@ -216,3 +232,24 @@ export enum SnapMode { NORMAL = 0, REAL = 1, } + +export const DefaultScoreMode = (v: any): ScoreMode => { + if (v === ScoreMode.FINAL || v === ScoreMode.ANY) { + return v + } + return ScoreMode.FINAL +} + +export const DefaultShapeMode = (v: any): ShapeMode => { + if (v === ShapeMode.NORMAL || v === ShapeMode.ANY || v === ShapeMode.FLAT) { + return v + } + return ShapeMode.NORMAL +} + +export const DefaultSnapMode = (v: any): SnapMode => { + if (v === SnapMode.NORMAL || v === SnapMode.REAL) { + return v + } + return SnapMode.NORMAL +} diff --git a/src/common/Util.ts b/src/common/Util.ts index 4077834..4de6495 100644 --- a/src/common/Util.ts +++ b/src/common/Util.ts @@ -130,9 +130,10 @@ function encodeGame(data: Game): EncodedGame { data.puzzle, data.players, data.evtInfos, - data.scoreMode || ScoreMode.FINAL, - data.shapeMode || ShapeMode.ANY, - data.snapMode || SnapMode.NORMAL, + data.scoreMode, + data.shapeMode, + data.snapMode, + data.creatorUserId, ] } @@ -149,6 +150,7 @@ function decodeGame(data: EncodedGame): Game { scoreMode: data[6], shapeMode: data[7], snapMode: data[8], + creatorUserId: data[9], } } diff --git a/src/dbpatches/02_image_sizes.sqlite b/src/dbpatches/02_image_sizes.sqlite new file mode 100644 index 0000000..35e0f39 --- /dev/null +++ b/src/dbpatches/02_image_sizes.sqlite @@ -0,0 +1,22 @@ +-- Add width/height to images table + +CREATE TABLE images_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + created TIMESTAMP NOT NULL, + + filename TEXT NOT NULL UNIQUE, + filename_original TEXT NOT NULL, + title TEXT NOT NULL, + + width INTEGER NOT NULL, + height INTEGER NOT NULL +); + +INSERT INTO images_new +SELECT id, created, filename, filename_original, title, 0, 0 +FROM images; + +DROP TABLE images; + +ALTER TABLE images_new RENAME TO images; diff --git a/src/dbpatches/03_users.sqlite b/src/dbpatches/03_users.sqlite new file mode 100644 index 0000000..c71908f --- /dev/null +++ b/src/dbpatches/03_users.sqlite @@ -0,0 +1,45 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + created TIMESTAMP NOT NULL, + + client_id TEXT NOT NULL, + client_secret TEXT NOT NULL +); + +CREATE TABLE images_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uploader_user_id INTEGER, + + created TIMESTAMP NOT NULL, + + filename TEXT NOT NULL UNIQUE, + filename_original TEXT NOT NULL, + title TEXT NOT NULL, + + width INTEGER NOT NULL, + height INTEGER NOT NULL +); + +CREATE TABLE image_x_category_new ( + image_id INTEGER NOT NULL, + category_id INTEGER NOT NULL +); + +INSERT INTO images_new +SELECT id, NULL, created, filename, filename_original, title, width, height +FROM images; + +INSERT INTO image_x_category_new +SELECT image_id, category_id +FROM image_x_category; + +PRAGMA foreign_keys = OFF; + +DROP TABLE images; +DROP TABLE image_x_category; + +ALTER TABLE images_new RENAME TO images; +ALTER TABLE image_x_category_new RENAME TO image_x_category; + +PRAGMA foreign_keys = ON; diff --git a/src/dbpatches/04_games.sqlite b/src/dbpatches/04_games.sqlite new file mode 100644 index 0000000..edde806 --- /dev/null +++ b/src/dbpatches/04_games.sqlite @@ -0,0 +1,11 @@ +CREATE TABLE games ( + id TEXT PRIMARY KEY, + + creator_user_id INTEGER, + image_id INTEGER NOT NULL, + + created TIMESTAMP NOT NULL, + finished TIMESTAMP NOT NULL, + + data TEXT NOT NULL +); diff --git a/src/frontend/App.vue b/src/frontend/App.vue index 7e13439..8a9c113 100644 --- a/src/frontend/App.vue +++ b/src/frontend/App.vue @@ -1,7 +1,7 @@ diff --git a/src/frontend/components/ImageTeaser.vue b/src/frontend/components/ImageTeaser.vue index 770de91..a5fd78d 100644 --- a/src/frontend/components/ImageTeaser.vue +++ b/src/frontend/components/ImageTeaser.vue @@ -3,7 +3,7 @@ class="imageteaser" :style="style" @click="onClick"> -
✏️
+
✏️
diff --git a/src/frontend/components/NewGameDialog.vue b/src/frontend/components/NewGameDialog.vue index 0042ade..eff46a7 100644 --- a/src/frontend/components/NewGameDialog.vue +++ b/src/frontend/components/NewGameDialog.vue @@ -6,7 +6,10 @@
-
"{{image.title}}"
+
+ "{{image.title}}" + ({{image.width}} βœ• {{image.height}}) +
@@ -18,27 +21,34 @@ - +
- + - +
- +
- + - +
- + @@ -142,7 +152,8 @@ export default defineComponent({ "image-title"; margin-right: 1em; } -@media (max-width: 1400px) { +@media (max-width: 1400px) and (min-height: 720px), + (max-width: 1000px) { .new-game-dialog .overlay-content { grid-template-columns: auto; grid-template-rows: 1fr min-content min-content; @@ -192,4 +203,8 @@ export default defineComponent({ top: .5em; left: .5em; } + +.new-game-dialog .image-title > span { margin-right: .5em; } +.new-game-dialog .image-title > span:last-child { margin-right: 0; } +.image-title-dim { display: inline-block; white-space: no-wrap; } diff --git a/src/frontend/components/NewImageDialog.vue b/src/frontend/components/NewImageDialog.vue index da3d7b0..c7cc832 100644 --- a/src/frontend/components/NewImageDialog.vue +++ b/src/frontend/components/NewImageDialog.vue @@ -14,6 +14,7 @@ gallery", if possible! @dragover="onDragover" @dragleave="onDragleave"> +
X @@ -48,10 +49,21 @@ gallery", if possible!
- - + +
-
@@ -74,6 +86,12 @@ export default defineComponent({ autocompleteTags: { type: Function, }, + uploadProgress: { + type: Number, + }, + uploading: { + type: String, + }, }, emits: { bgclick: null, @@ -90,10 +108,19 @@ export default defineComponent({ } }, computed: { + uploadProgressPercent (): number { + return this.uploadProgress ? Math.round(this.uploadProgress * 100) : 0 + }, canPostToGallery (): boolean { + if (this.uploading) { + return false + } return !!(this.previewUrl && this.file) }, canSetupGameClick (): boolean { + if (this.uploading) { + return false + } return !!(this.previewUrl && this.file) }, }, @@ -183,7 +210,8 @@ export default defineComponent({ height: 90%; width: 80%; } -@media (max-width: 1400px) { +@media (max-width: 1400px) and (min-height: 720px), + (max-width: 1000px) { .new-image-dialog .overlay-content { grid-template-columns: auto; grid-template-rows: 1fr min-content min-content; @@ -212,9 +240,6 @@ export default defineComponent({ .new-image-dialog .area-image.droppable { border: dashed 6px; } -.area-image * { - pointer-events: none; -} .new-image-dialog .area-image .has-image { position: relative; width: 100%; @@ -256,4 +281,16 @@ export default defineComponent({ top: 50%; transform: translate(-50%,-50%); } +.area-image .drop-target { + display: none; +} +.area-image.droppable .drop-target { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 3; +} diff --git a/src/frontend/components/SettingsOverlay.vue b/src/frontend/components/SettingsOverlay.vue index 01155e4..b17c4dd 100644 --- a/src/frontend/components/SettingsOverlay.vue +++ b/src/frontend/components/SettingsOverlay.vue @@ -17,6 +17,24 @@ + + + + πŸ”‰ + + πŸ”Š + + + + + + @@ -30,7 +48,23 @@ export default defineComponent({ 'update:modelValue': null, }, props: { - modelValue: Object, + modelValue: { + type: Object, + required: true, + }, + }, + methods: { + updateVolume (ev: Event): void { + (this.modelValue as any).soundsVolume = (ev.target as HTMLInputElement).value + }, + decreaseVolume (): void { + const vol = parseInt(this.modelValue.soundsVolume, 10) - 5 + this.modelValue.soundsVolume = Math.max(0, vol) + }, + increaseVolume (): void { + const vol = parseInt(this.modelValue.soundsVolume, 10) + 5 + this.modelValue.soundsVolume = Math.min(100, vol) + }, }, created () { // TODO: ts type PlayerSettings @@ -40,3 +74,7 @@ export default defineComponent({ }, }) + diff --git a/src/frontend/components/TagsInput.vue b/src/frontend/components/TagsInput.vue index e0d4f1f..0fbbcdc 100644 --- a/src/frontend/components/TagsInput.vue +++ b/src/frontend/components/TagsInput.vue @@ -56,14 +56,14 @@ export default defineComponent({ }, methods: { onKeyUp (ev: KeyboardEvent) { - if (ev.key === 'ArrowDown' && this.autocomplete.values.length > 0) { + if (ev.code === 'ArrowDown' && this.autocomplete.values.length > 0) { if (this.autocomplete.idx < this.autocomplete.values.length - 1) { this.autocomplete.idx++ } ev.stopPropagation() return false } - if (ev.key === 'ArrowUp' && this.autocomplete.values.length > 0) { + if (ev.code === 'ArrowUp' && this.autocomplete.values.length > 0) { if (this.autocomplete.idx > 0) { this.autocomplete.idx-- } diff --git a/src/frontend/components/Upload.vue b/src/frontend/components/Upload.vue index a5a6949..ec030c6 100644 --- a/src/frontend/components/Upload.vue +++ b/src/frontend/components/Upload.vue @@ -6,6 +6,7 @@