diff --git a/build/public/assets/index.643c957c.js b/build/public/assets/index.643c957c.js new file mode 100644 index 0000000..77b35d4 --- /dev/null +++ b/build/public/assets/index.643c957c.js @@ -0,0 +1 @@ +import{d as e,c as t,a as n,w as l,b as i,r as o,o as a,e as s,t as r,F as d,f as c,g as u,h as g,v as p,i as h,j as m,k as y,l as f,m as w,n as v,p as b,q as x,s as C}from"./vendor.b622ee49.js";var k=e({name:"app",computed:{showNav(){return!["game","replay"].includes(String(this.$route.name))}}});const A={id:"app"},z={key:0,class:"nav"},S=s("Index"),P=s("New game");k.render=function(e,s,r,d,c,u){const g=o("router-link"),p=o("router-view");return a(),t("div",A,[e.showNav?(a(),t("ul",z,[n("li",null,[n(g,{class:"btn",to:{name:"index"}},{default:l((()=>[S])),_:1})]),n("li",null,[n(g,{class:"btn",to:{name:"new-game"}},{default:l((()=>[P])),_:1})])])):i("",!0),n(p)])};const I=864e5,T=e=>{const t=Math.floor(e/I);e%=I;const n=Math.floor(e/36e5);e%=36e5;const l=Math.floor(e/6e4);e%=6e4;return`${t}d ${n}h ${l}m ${Math.floor(e/1e3)}s`};var _=1e3,D=()=>{const e=new Date;return Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())},B=(e,t)=>T(t-e),E=T,O=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?"🏁":"⏳",l=e,i=t||D();return`${n} ${B(l,i)}`}}});const U={class:"game-info-text"},N=n("br",null,null,-1),M=n("br",null,null,-1),G=n("br",null,null,-1),$=s(" β†ͺ️ Watch replay ");O.render=function(e,d,c,u,g,p){const h=o("router-link");return a(),t("div",{class:"game-teaser",style:e.style},[n(h,{class:"game-info",to:{name:"game",params:{id:e.game.id}}},{default:l((()=>[n("span",U,[s(" 🧩 "+r(e.game.tilesFinished)+"/"+r(e.game.tilesTotal),1),N,s(" πŸ‘₯ "+r(e.game.players),1),M,s(" "+r(e.time(e.game.started,e.game.finished)),1),G])])),_:1},8,["to"]),e.game.hasReplay?(a(),t(h,{key:0,class:"game-replay",to:{name:"replay",params:{id:e.game.id}}},{default:l((()=>[$])),_:1},8,["to"])):i("",!0)],4)};var R=e({components:{GameTeaser:O},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 V=n("h1",null,"Running games",-1),j=n("h1",null,"Finished games",-1);R.render=function(e,l,i,s,r,u){const g=o("game-teaser");return a(),t("div",null,[V,(a(!0),t(d,null,c(e.gamesRunning,((e,l)=>(a(),t("div",{class:"game-teaser-wrap",key:l},[n(g,{game:e},null,8,["game"])])))),128)),j,(a(!0),t(d,null,c(e.gamesFinished,((e,l)=>(a(),t("div",{class:"game-teaser-wrap",key:l},[n(g,{game:e},null,8,["game"])])))),128))])};var F=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")}}});F.render=function(e,l,i,o,s,r){return a(),t("div",{class:"imageteaser",style:e.style,onClick:l[2]||(l[2]=(...t)=>e.onClick&&e.onClick(...t))},[n("div",{class:"btn edit",onClick:l[1]||(l[1]=u(((...t)=>e.onEditClick&&e.onEditClick(...t)),["stop"]))},"✏️")],4)};var L=e({name:"image-library",components:{ImageTeaser:F},props:{images:{type:Array,required:!0}},emits:{imageClicked:null,imageEditClicked:null},methods:{imageClicked(e){this.$emit("imageClicked",e)},imageEditClicked(e){this.$emit("imageEditClicked",e)}}});L.render=function(e,n,l,i,s,r){const u=o("image-teaser");return a(),t("div",null,[(a(!0),t(d,null,c(e.images,((n,l)=>(a(),t(u,{image:n,onClick:t=>e.imageClicked(n),onEditClick:t=>e.imageEditClicked(n),key:l},null,8,["image","onClick","onEditClick"])))),128))])};const W={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}}}};W.render=function(e,n,l,i,o,s){return a(),t("div",{style:s.style,title:l.title},null,12,["title"])};var q=e({name:"tags-input",props:{modelValue:{type:Array,required:!0}},emits:{"update:modelValue":null},data:()=>({input:"",values:[]}),created(){this.values=this.modelValue},methods:{onKeyUp(e){if(","===e.key)return this.add(),e.stopPropagation(),!1},add(){const e=this.input.replace(/,/g,"").trim();e&&(this.values.includes(e)||this.values.push(e),this.input="",this.$emit("update:modelValue",this.values))},rm(e){this.values=this.values.filter((t=>t!==e)),this.$emit("update:modelValue",this.values)}}});const H=m()(((e,l,i,o,s,u)=>(a(),t("div",null,[g(n("input",{class:"input",type:"text","onUpdate:modelValue":l[1]||(l[1]=t=>e.input=t),placeholder:"Plants, People",onKeydown:l[2]||(l[2]=h(((...t)=>e.add&&e.add(...t)),["enter"])),onKeyup:l[3]||(l[3]=(...t)=>e.onKeyUp&&e.onKeyUp(...t))},null,544),[[p,e.input]]),(a(!0),t(d,null,c(e.values,((n,l)=>(a(),t("span",{key:l,class:"bit",onClick:t=>e.rm(n)},r(n)+" βœ–",9,["onClick"])))),128))]))));q.render=H,q.__scopeId="data-v-771460ae";var Q=e({name:"new-image-dialog",components:{ResponsiveImage:W,TagsInput:q},emits:{bgclick:null,setupGameClick:null,postToGalleryClick:null},data:()=>({previewUrl:"",file:null,title:"",tags:[]}),computed:{canPostToGallery(){return!(!this.previewUrl||!this.file)},canSetupGameClick(){return!(!this.previewUrl||!this.file)}},methods:{preview(e){const t=e.target;if(!t.files)return;const n=t.files[0];if(!n)return;const l=new FileReader;l.readAsDataURL(n),l.onload=e=>{this.previewUrl=e.target.result,this.file=n}},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})}}});const Y={key:0,class:"has-image"},K={key:1},Z={class:"upload"},J=n("span",{class:"btn"},"Upload File",-1),X={class:"area-settings"},ee=n("td",null,[n("label",null,"Title")],-1),te=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),le={class:"area-buttons"},ie=s("🧩 Post to gallery "),oe=n("br",null,null,-1),ae=s(" + set up game");Q.render=function(e,l,i,s,r,d){const c=o("responsive-image"),h=o("tags-input");return a(),t("div",{class:"overlay new-image-dialog",onClick:l[8]||(l[8]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:l[7]||(l[7]=u((()=>{}),["stop"]))},[n("div",{class:["area-image",{"has-image":!!e.previewUrl,"no-image":!e.previewUrl}]},[e.previewUrl?(a(),t("div",Y,[n("span",{class:"remove btn",onClick:l[1]||(l[1]=t=>e.previewUrl="")},"X"),n(c,{src:e.previewUrl},null,8,["src"])])):(a(),t("div",K,[n("label",Z,[n("input",{type:"file",style:{display:"none"},onChange:l[2]||(l[2]=(...t)=>e.preview&&e.preview(...t)),accept:"image/*"},null,32),J])]))],2),n("div",X,[n("table",null,[n("tr",null,[ee,n("td",null,[g(n("input",{type:"text","onUpdate:modelValue":l[3]||(l[3]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[p,e.title]])])]),te,n("tr",null,[ne,n("td",null,[n(h,{modelValue:e.tags,"onUpdate:modelValue":l[4]||(l[4]=t=>e.tags=t)},null,8,["modelValue"])])])])]),n("div",le,[n("button",{class:"btn",disabled:!e.canPostToGallery,onClick:l[5]||(l[5]=(...t)=>e.postToGallery&&e.postToGallery(...t))},"πŸ–ΌοΈ Post to gallery",8,["disabled"]),n("button",{class:"btn",disabled:!e.canSetupGameClick,onClick:l[6]||(l[6]=(...t)=>e.setupGameClick&&e.setupGameClick(...t))},[ie,oe,ae],8,["disabled"])])])])};var se=e({name:"edit-image-dialog",components:{ResponsiveImage:W,TagsInput:q},props:{image:{type:Object,required:!0}},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 re={class:"area-image"},de={class:"has-image"},ce={class:"area-settings"},ue=n("td",null,[n("label",null,"Title")],-1),ge=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),pe=n("td",null,[n("label",null,"Tags")],-1),he={class:"area-buttons"};function me(e,t){const n=e.x-t.x,l=e.y-t.y;return Math.sqrt(n*n+l*l)}function ye(e){return{x:e.x+e.w/2,y:e.y+e.h/2}}se.render=function(e,l,i,s,r,d){const c=o("responsive-image"),h=o("tags-input");return a(),t("div",{class:"overlay edit-image-dialog",onClick:l[5]||(l[5]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:l[4]||(l[4]=u((()=>{}),["stop"]))},[n("div",re,[n("div",de,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",ce,[n("table",null,[n("tr",null,[ue,n("td",null,[g(n("input",{type:"text","onUpdate:modelValue":l[1]||(l[1]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[p,e.title]])])]),ge,n("tr",null,[pe,n("td",null,[n(h,{modelValue:e.tags,"onUpdate:modelValue":l[2]||(l[2]=t=>e.tags=t)},null,8,["modelValue"])])])])]),n("div",he,[n("button",{class:"btn",onClick:l[3]||(l[3]=(...t)=>e.saveImage&&e.saveImage(...t))},"πŸ–ΌοΈ Save image")])])])};var fe={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:me,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:ye,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 me(ye(e),ye(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)}};var we=1,ve=4,be=2,xe=3,Ce=2,ke=4,Ae=3,ze=9,Se=1,Pe=2,Ie=3,Te=4,_e=5,De=6,Be=7,Ee=8,Oe=10,Ue=1,Ne=2,Me=3;class Ge{constructor(e){this.rand_high=e||3735929054,this.rand_low=1231121986^e}random(e,t){return 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,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),l=t[n];t[n]=t[e],t[e]=l}return t}static serialize(e){return{rand_high:e.rand_high,rand_low:e.rand_low}}static unserialize(e){const t=new Ge(0);return t.rand_high=e.rand_high,t.rand_low=e.rand_low,t}}const $e=(e,t)=>{const n=`${e}`;return n.length>=t.length?n:t.substr(0,t.length-n.length)+n},Re=(...e)=>{const t=t=>(...n)=>{const l=new Date,i=$e(l.getHours(),"00"),o=$e(l.getMinutes(),"00"),a=$e(l.getSeconds(),"00");console[t](`${i}:${o}:${a}`,...e,...n)};return{log:t("log"),error:t("error"),info:t("info")}};var Ve,je,Fe,Le,We={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||"",Ge.serialize(e.rng.obj),e.puzzle,e.players,e.evtInfos,e.scoreMode||Fe.FINAL]},decodeGame:function(e){return{id:e[0],rng:{type:e[1],obj:Ge.unserialize(e[2])},puzzle:e[3],players:e[4],evtInfos:e[5],scoreMode:e[6]}},coordByTileIdx:function(e,t){const n=e.width/e.tileSize;return{x:t%n,y:Math.floor(t/n)}},asQueryArgs:function(e){const t=[];for(let n in e){const l=[n,e[n]].map(encodeURIComponent);t.push(l.join("="))}return 0===t.length?"":`?${t.join("&")}`}};(je=Ve||(Ve={}))[je.Flat=0]="Flat",je[je.Out=1]="Out",je[je.In=-1]="In",(Le=Fe||(Fe={}))[Le.FINAL=0]="FINAL",Le[Le.ANY=1]="ANY";const qe={};function He(e,t){let n=0;for(let l of qe[e].players){if(We.decodePlayer(l).id===t)return n;n++}return-1}function Qe(e,t){const n=He(e,t);return-1===n?null:We.decodePlayer(qe[e].players[n])}function Ye(e,t,n){const l=He(e,t);-1===l?qe[e].players.push(We.encodePlayer(n)):qe[e].players[l]=We.encodePlayer(n)}function Ke(e,t){return-1!==He(e,t)}function Ze(e){return qe[e]?qe[e].players.map(We.decodePlayer):[]}function Je(e){return qe[e].puzzle.tiles.length}function Xe(e){return qe[e].scoreMode||0}function et(e){return tt(e)===Je(e)}function tt(e){let t=0;for(let n of qe[e].puzzle.tiles)-1===We.decodePiece(n).owner&&t++;return t}function nt(e,t,n){const l=Qe(e,t);if(null!==l){for(let e of Object.keys(n))l[e]=n[e];Ye(e,t,l)}}function lt(e,t){for(let n of Object.keys(t))qe[e].puzzle.data[n]=t[n]}function it(e,t,n){for(let l of Object.keys(n)){const i=We.decodePiece(qe[e].puzzle.tiles[t]);i[l]=n[l],qe[e].puzzle.tiles[t]=We.encodePiece(i)}}const ot=(e,t)=>We.decodePiece(qe[e].puzzle.tiles[t]),at=(e,t)=>ot(e,t).group,st=(e,t)=>{const n=qe[e].puzzle.info,l={x:(n.table.width-n.width)/2,y:(n.table.height-n.height)/2},i=function(e,t){const n=qe[e].puzzle.info,l=We.coordByTileIdx(n,t),i=l.x*n.tileSize,o=l.y*n.tileSize;return{x:i,y:o}}(e,t);return fe.pointAdd(l,i)},rt=(e,t)=>ot(e,t).pos,dt=e=>{const t=At(e),n=zt(e),l=Math.round(t/4),i=Math.round(n/4);return{x:0-l,y:0-i,w:t+2*l,h:n+2*i}},ct=(e,t)=>{const n=ht(e),l=ot(e,t);return{x:l.pos.x,y:l.pos.y,w:n,h:n}},ut=(e,t)=>ot(e,t).z,gt=(e,t)=>{for(let n of qe[e].puzzle.tiles){const e=We.decodePiece(n);if(e.owner===t)return e.idx}return-1},pt=e=>qe[e].puzzle.info.tileDrawSize,ht=e=>qe[e].puzzle.info.tileSize,mt=e=>qe[e].puzzle.data.maxGroup,yt=e=>qe[e].puzzle.data.maxZ;function ft(e,t){const n=qe[e].puzzle.info,l=We.coordByTileIdx(n,t);return[l.y>0?t-n.tilesX:-1,l.x0?t-1:-1]}const wt=(e,t,n)=>{for(let l of t)it(e,l,{z:n})},vt=(e,t,n)=>{const l=rt(e,t);it(e,t,{pos:fe.pointAdd(l,n)})},bt=(e,t,n)=>{const l=pt(e),i=dt(e),o=n;for(let a of t){const t=ot(e,a);t.pos.x+n.xi.x+i.w&&(o.x=Math.min(i.x+i.w-t.pos.x+l,o.x)),t.pos.y+n.yi.y+i.h&&(o.y=Math.min(i.y+i.h-t.pos.y+l,o.y))}for(let a of t)vt(e,a,o)},xt=(e,t,n)=>{for(let l of t)it(e,l,{owner:n})};function Ct(e,t){const n=qe[e].puzzle.tiles,l=We.decodePiece(n[t]),i=[];if(l.group)for(let o of n){const e=We.decodePiece(o);e.group===l.group&&i.push(e.idx)}else i.push(l.idx);return i}const kt=(e,t)=>{const n=Qe(e,t);return n?n.points:0},At=e=>qe[e].puzzle.info.table.width,zt=e=>qe[e].puzzle.info.table.height;var St={setGame:function(e,t){qe[e]=t},exists:function(e){return!!qe[e]||!1},playerExists:Ke,getActivePlayers:function(e,t){const n=t-30*_;return Ze(e).filter((e=>e.ts>=n))},getIdlePlayers:function(e,t){const n=t-30*_;return Ze(e).filter((e=>e.ts0))},addPlayer:function(e,t,n){Ke(e,t)?nt(e,t,{ts:n}):Ye(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:tt,getPieceCount:Je,getImageUrl:function(e){return qe[e].puzzle.info.imageUrl},setImageUrl:function(e,t){qe[e].puzzle.info.imageUrl=t},get:function(e){return qe[e]},getAllGames:function(){return Object.values(qe).sort(((e,t)=>et(e.id)===et(t.id)?t.puzzle.data.started-e.puzzle.data.started:et(e.id)?1:-1))},getPlayerBgColor:(e,t)=>{const n=Qe(e,t);return n?n.bgcolor:null},getPlayerColor:(e,t)=>{const n=Qe(e,t);return n?n.color:null},getPlayerName:(e,t)=>{const n=Qe(e,t);return n?n.name:null},getPlayerIndexById:He,getPlayerIdByIndex:function(e,t){return qe[e].players.length>t?We.decodePlayer(qe[e].players[t]).id:null},changePlayer:nt,setPlayer:Ye,setPiece:function(e,t,n){qe[e].puzzle.tiles[t]=We.encodePiece(n)},setPuzzleData:function(e,t){qe[e].puzzle.data=t},getTableWidth:At,getTableHeight:zt,getPuzzle:e=>qe[e].puzzle,getRng:e=>qe[e].rng.obj,getPuzzleWidth:e=>qe[e].puzzle.info.width,getPuzzleHeight:e=>qe[e].puzzle.info.height,getPiecesSortedByZIndex:function(e){return qe[e].puzzle.tiles.map(We.decodePiece).sort(((e,t)=>e.z-t.z))},getFirstOwnedPiece:(e,t)=>{const n=gt(e,t);return n<0?null:qe[e].puzzle.tiles[n]},getPieceDrawOffset:e=>qe[e].puzzle.info.tileDrawOffset,getPieceDrawSize:pt,getFinalPiecePos:st,getStartTs:e=>qe[e].puzzle.data.started,getFinishTs:e=>qe[e].puzzle.data.finished,handleInput:function(e,t,n,l){const i=qe[e].puzzle,o=function(e,t){return t in qe[e].evtInfos?qe[e].evtInfos[t]:{_last_mouse:null,_last_mouse_down:null}}(e,t),a=[],s=()=>{a.push([Ue,i.data])},r=t=>{a.push([Ne,We.encodePiece(ot(e,t))])},d=e=>{for(const t of e)r(t)},c=()=>{const n=Qe(e,t);n&&a.push([Me,We.encodePlayer(n)])},u=n[0];if(u===De){const i=n[1];nt(e,t,{bgcolor:i,ts:l}),c()}else if(u===Be){const i=n[1];nt(e,t,{color:i,ts:l}),c()}else if(u===Ee){const i=`${n[1]}`.substr(0,16);nt(e,t,{name:i,ts:l}),c()}else if(u===Se){const i={x:n[1],y:n[2]};nt(e,t,{d:1,ts:l}),c(),o._last_mouse_down=i;const a=((e,t)=>{let n=qe[e].puzzle.info,l=qe[e].puzzle.tiles,i=-1,o=-1;for(let a=0;ai)&&(i=e.z,o=a)}return o})(e,i);if(a>=0){let n=yt(e)+1;lt(e,{maxZ:n}),s();const l=Ct(e,a);wt(e,l,yt(e)),xt(e,l,t),d(l)}o._last_mouse=i}else if(u===Ie){const i=n[1],a=n[2],s={x:i,y:a};if(null===o._last_mouse_down)nt(e,t,{x:i,y:a,ts:l}),c();else{let n=gt(e,t);if(n>=0){nt(e,t,{x:i,y:a,ts:l}),c();const r=Ct(e,n);let u=fe.pointInBounds(s,dt(e))&&fe.pointInBounds(o._last_mouse_down,dt(e));for(let t of r){const n=ct(e,t);if(fe.pointInBounds(s,n)){u=!0;break}}if(u){const t=i-o._last_mouse_down.x,n=a-o._last_mouse_down.y;bt(e,r,{x:t,y:n}),d(r)}}else nt(e,t,{ts:l}),c();o._last_mouse_down=s}o._last_mouse=s}else if(u===Pe){const a={x:n[1],y:n[2]},u=0;o._last_mouse_down=null;let g=gt(e,t);if(g>=0){let n=Ct(e,g);xt(e,n,0),d(n);let o=rt(e,g),a=st(e,g);if(fe.pointDistance(a,o){for(let n of t)it(e,n,{owner:-1,z:1})})(e,n),d(n);let r=kt(e,t);0===Xe(e)?r+=n.length:1===Xe(e)&&(r+=1),nt(e,t,{d:u,ts:l,points:r}),c(),tt(e)===Je(e)&&(lt(e,{finished:l}),s())}else{const n=(e,t,n,l)=>{let i=qe[e].puzzle.info;if(n<0)return!1;if(((e,t,n)=>{const l=at(e,t),i=at(e,n);return!(!l||l!==i)})(e,t,n))return!1;const o=rt(e,t),a=fe.pointAdd(rt(e,n),{x:l[0]*i.tileSize,y:l[1]*i.tileSize});if(fe.pointDistance(o,a){const l=qe[e].puzzle.tiles,i=at(e,t),o=at(e,n);let a;const d=[];i&&d.push(i),o&&d.push(o),i?a=i:o?a=o:(lt(e,{maxGroup:mt(e)+1}),s(),a=mt(e));if(it(e,t,{group:a}),r(t),it(e,n,{group:a}),r(n),d.length>0)for(const s of l){const t=We.decodePiece(s);d.includes(t.group)&&(it(e,t.idx,{group:a}),r(t.idx))}})(e,t,n),i=Ct(e,t);const c=((e,t)=>{let n=0;for(let l of t){let t=ut(e,l);t>n&&(n=t)}return n})(e,i);return wt(e,i,c),d(i),!0}return!1};let i=!1;for(let t of Ct(e,g)){let l=ft(e,t);if(n(e,t,l[0],[0,1])||n(e,t,l[1],[-1,0])||n(e,t,l[2],[0,-1])||n(e,t,l[3],[1,0])){i=!0;break}}if(i&&1===Xe(e)){const n=kt(e,t)+1;nt(e,t,{d:u,ts:l,points:n}),c()}else nt(e,t,{d:u,ts:l}),c()}}else nt(e,t,{d:u,ts:l}),c();o._last_mouse=a}else if(u===Te){const i=n[1],a=n[2];nt(e,t,{x:i,y:a,ts:l}),c(),o._last_mouse={x:i,y:a}}else if(u===_e){const i=n[1],a=n[2];nt(e,t,{x:i,y:a,ts:l}),c(),o._last_mouse={x:i,y:a}}else nt(e,t,{ts:l}),c();return function(e,t,n){qe[e].evtInfos[t]=n}(e,t,o),a}},Pt=e({name:"new-game-dialog",components:{ResponsiveImage:W},props:{image:{type:Object,required:!0}},emits:{newGame:null,bgclick:null},data:()=>({tiles:1e3,scoreMode:Fe.ANY}),methods:{onNewGameClick(){this.$emit("newGame",{tiles:this.tilesInt,image:this.image,scoreMode:this.scoreModeInt})}},computed:{canStartNewGame(){return!!(this.tilesInt&&this.image&&this.image.url&&[0,1].includes(this.scoreModeInt))},scoreModeInt(){return parseInt(`${this.scoreMode}`,10)},tilesInt(){return parseInt(`${this.tiles}`,10)}}});const It={class:"area-image"},Tt={class:"has-image"},_t={class:"area-settings"},Dt=n("td",null,[n("label",null,"Pieces")],-1),Bt=n("td",null,[n("label",null,"Scoring: ")],-1),Et=s(" Any (Score when pieces are connected to each other or on final location)"),Ot=n("br",null,null,-1),Ut=s(" Final (Score when pieces are put to their final location)"),Nt={class:"area-buttons"};Pt.render=function(e,l,i,s,r,d){const c=o("responsive-image");return a(),t("div",{class:"overlay new-game-dialog",onClick:l[6]||(l[6]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:l[5]||(l[5]=u((()=>{}),["stop"]))},[n("div",It,[n("div",Tt,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",_t,[n("table",null,[n("tr",null,[Dt,n("td",null,[g(n("input",{type:"text","onUpdate:modelValue":l[1]||(l[1]=t=>e.tiles=t)},null,512),[[p,e.tiles]])])]),n("tr",null,[Bt,n("td",null,[n("label",null,[g(n("input",{type:"radio","onUpdate:modelValue":l[2]||(l[2]=t=>e.scoreMode=t),value:"1"},null,512),[[y,e.scoreMode]]),Et]),Ot,n("label",null,[g(n("input",{type:"radio","onUpdate:modelValue":l[3]||(l[3]=t=>e.scoreMode=t),value:"0"},null,512),[[y,e.scoreMode]]),Ut])])])])]),n("div",Nt,[n("button",{class:"btn",disabled:!e.canStartNewGame,onClick:l[4]||(l[4]=(...t)=>e.onNewGameClick&&e.onNewGameClick(...t))}," 🧩 Generate Puzzle ",8,["disabled"])])])])};var Mt=e({components:{ImageLibrary:L,NewImageDialog:Q,EditImageDialog:se,NewGameDialog:Pt},data:()=>({filters:{sort:"date_desc",tags:[]},images:[],tags:[],image:{id:0,filename:"",file:"",url:"",title:"",tags:[],created:0},dialog:""}),async created(){await this.loadImages()},methods:{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${We.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("/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 Gt={class:"upload-image-teaser"},$t=n("div",{class:"hint"},"(The image you upload will be added to the public gallery.)",-1),Rt={key:0},Vt=s(" Tags: "),jt=s(" Sort by: "),Ft=n("option",{value:"date_desc"},"Newest first",-1),Lt=n("option",{value:"date_asc"},"Oldest first",-1),Wt=n("option",{value:"alpha_asc"},"A-Z",-1),qt=n("option",{value:"alpha_desc"},"Z-A",-1);Mt.render=function(e,l,s,u,p,h){const m=o("image-library"),y=o("new-image-dialog"),w=o("edit-image-dialog"),v=o("new-game-dialog");return a(),t("div",null,[n("div",Gt,[n("div",{class:"btn btn-big",onClick:l[1]||(l[1]=t=>e.dialog="new-image")},"Upload your image"),$t]),n("div",null,[e.tags.length>0?(a(),t("label",Rt,[Vt,(a(!0),t(d,null,c(e.tags,((n,l)=>(a(),t("span",{class:["bit",{on:e.filters.tags.includes(n.slug)}],key:l,onClick:t=>e.toggleTag(n)},r(n.title),11,["onClick"])))),128))])):i("",!0),n("label",null,[jt,g(n("select",{"onUpdate:modelValue":l[2]||(l[2]=t=>e.filters.sort=t),onChange:l[3]||(l[3]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[Ft,Lt,Wt,qt],544),[[f,e.filters.sort]])])]),n(m,{images:e.images,onImageClicked:e.onImageClicked,onImageEditClicked:e.onImageEditClicked},null,8,["images","onImageClicked","onImageEditClicked"]),"new-image"===e.dialog?(a(),t(y,{key:0,onBgclick:l[4]||(l[4]=t=>e.dialog=""),onPostToGalleryClick:e.postToGalleryClick,onSetupGameClick:e.setupGameClick},null,8,["onPostToGalleryClick","onSetupGameClick"])):i("",!0),"edit-image"===e.dialog?(a(),t(w,{key:1,onBgclick:l[5]||(l[5]=t=>e.dialog=""),onSaveClick:e.onSaveImageClick,image:e.image},null,8,["onSaveClick","image"])):i("",!0),e.image&&"new-game"===e.dialog?(a(),t(v,{key:2,onBgclick:l[6]||(l[6]=t=>e.dialog=""),onNewGame:e.onNewGame,image:e.image},null,8,["onNewGame","image"])):i("",!0)])};var Ht=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 Qt={class:"scores"},Yt=n("div",null,"Scores",-1),Kt=n("td",null,"⚑",-1),Zt=n("td",null,"πŸ’€",-1);Ht.render=function(e,l,i,o,s,u){return a(),t("div",Qt,[Yt,n("table",null,[(a(!0),t(d,null,c(e.actives,((e,l)=>(a(),t("tr",{key:l,style:{color:e.color}},[Kt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128)),(a(!0),t(d,null,c(e.idles,((e,l)=>(a(),t("tr",{key:l,style:{color:e.color}},[Zt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128))])])};var Jt=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 E(this.duration)}}});const Xt={class:"timer"};Jt.render=function(e,l,i,o,s,d){return a(),t("div",Xt,[n("div",null," 🧩 "+r(e.piecesDone)+"/"+r(e.piecesTotal),1),n("div",null,r(e.icon)+" "+r(e.durationStr),1),w(e.$slots,"default")])};var en=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 tn=n("td",null,[n("label",null,"Background: ")],-1),nn=n("td",null,[n("label",null,"Color: ")],-1),ln=n("td",null,[n("label",null,"Name: ")],-1);en.render=function(e,l,i,o,s,r){return a(),t("div",{class:"overlay transparent",onClick:l[5]||(l[5]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content settings",onClick:l[4]||(l[4]=u((()=>{}),["stop"]))},[n("tr",null,[tn,n("td",null,[g(n("input",{type:"color","onUpdate:modelValue":l[1]||(l[1]=t=>e.modelValue.background=t)},null,512),[[p,e.modelValue.background]])])]),n("tr",null,[nn,n("td",null,[g(n("input",{type:"color","onUpdate:modelValue":l[2]||(l[2]=t=>e.modelValue.color=t)},null,512),[[p,e.modelValue.color]])])]),n("tr",null,[ln,n("td",null,[g(n("input",{type:"text",maxLength:"16","onUpdate:modelValue":l[3]||(l[3]=t=>e.modelValue.name=t)},null,512),[[p,e.modelValue.name]])])])])])};var on=e({name:"preview-overlay",props:{img:String},emits:{bgclick:null},computed:{previewStyle(){return{backgroundImage:`url('${this.img}')`}}}});const an={class:"preview"};on.render=function(e,l,i,o,s,r){return a(),t("div",{class:"overlay",onClick:l[1]||(l[1]=t=>e.$emit("bgclick"))},[n("div",an,[n("div",{class:"img",style:e.previewStyle},null,4)])])};const sn=Re("Communication.js");let rn,dn=e=>{},cn=e=>{};let un=0;const gn=e=>{un!==e&&(un=e,cn(e))};function pn(e){if(2===un)try{rn.send(JSON.stringify(e))}catch(t){sn.info("unable to send message.. maybe because ws is invalid?")}}let hn,mn;var yn={connect:function(e,t,n){return hn=0,mn={},gn(3),new Promise((l=>{rn=new WebSocket(e,n+"|"+t),rn.onopen=e=>{gn(2),pn([xe])},rn.onmessage=e=>{const t=JSON.parse(e.data),i=t[0];if(i===ve){const e=t[1];l(e)}else{if(i!==we)throw`[ 2021-05-09 invalid connect msgType ${i} ]`;{const e=t[1],l=t[2];if(e===n&&mn[l])return void delete mn[l];dn(t)}}},rn.onerror=e=>{throw gn(1),"[ 2021-05-15 onerror ]"},rn.onclose=e=>{4e3===e.code||1001===e.code?gn(4):gn(1)}}))},requestReplayData:async function(e,t,n){const l={gameId:e,offset:t,size:n},i=await fetch(`/api/replay-data${We.asQueryArgs(l)}`);return await i.json()},disconnect:function(){rn&&rn.close(4e3),hn=0,mn={}},sendClientEvent:function(e){hn++,mn[hn]=e,pn([be,hn,mn[hn]])},onServerChange:function(e){dn=e},onConnectionStateChange:function(e){cn=e},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},fn=e({name:"connection-overlay",emits:{reconnect:null},props:{connectionState:Number},computed:{lostConnection(){return this.connectionState===yn.CONN_STATE_DISCONNECTED},connecting(){return this.connectionState===yn.CONN_STATE_CONNECTING},show(){return!(!this.lostConnection&&!this.connecting)}}});const wn={key:0,class:"overlay connection-lost"},vn={key:0,class:"overlay-content"},bn=n("div",null,"⁉️ LOST CONNECTION ⁉️",-1),xn={key:1,class:"overlay-content"},Cn=n("div",null,"Connecting...",-1);fn.render=function(e,l,o,s,r,d){return e.show?(a(),t("div",wn,[e.lostConnection?(a(),t("div",vn,[bn,n("span",{class:"btn",onClick:l[1]||(l[1]=t=>e.$emit("reconnect"))},"Reconnect")])):i("",!0),e.connecting?(a(),t("div",xn,[Cn])):i("",!0)])):i("",!0)};var kn=e({name:"help-overlay",emits:{bgclick:null}});const An=n("tr",null,[n("td",null,"⬆️ Move up:"),n("td",null,[n("div",null,[n("kbd",null,"W"),s("/"),n("kbd",null,"↑"),s("/πŸ–±οΈ")])])],-1),zn=n("tr",null,[n("td",null,"⬇️ Move down:"),n("td",null,[n("div",null,[n("kbd",null,"S"),s("/"),n("kbd",null,"↓"),s("/πŸ–±οΈ")])])],-1),Sn=n("tr",null,[n("td",null,"⬅️ Move left:"),n("td",null,[n("div",null,[n("kbd",null,"A"),s("/"),n("kbd",null,"←"),s("/πŸ–±οΈ")])])],-1),Pn=n("tr",null,[n("td",null,"➑️ Move right:"),n("td",null,[n("div",null,[n("kbd",null,"D"),s("/"),n("kbd",null,"β†’"),s("/πŸ–±οΈ")])])],-1),In=n("tr",null,[n("td"),n("td",null,[n("div",null,[s("Move faster by holding "),n("kbd",null,"Shift")])])],-1),Tn=n("tr",null,[n("td",null,"πŸ”+ Zoom in:"),n("td",null,[n("div",null,[n("kbd",null,"E"),s("/πŸ–±οΈ-Wheel")])])],-1),_n=n("tr",null,[n("td",null,"πŸ”- Zoom out:"),n("td",null,[n("div",null,[n("kbd",null,"Q"),s("/πŸ–±οΈ-Wheel")])])],-1),Dn=n("tr",null,[n("td",null,"πŸ–ΌοΈ Toggle preview:"),n("td",null,[n("div",null,[n("kbd",null,"Space")])])],-1),Bn=n("tr",null,[n("td",null,"πŸ§©βœ”οΈ Toggle fixed pieces:"),n("td",null,[n("div",null,[n("kbd",null,"F")])])],-1),En=n("tr",null,[n("td",null,"πŸ§©β“ Toggle loose pieces:"),n("td",null,[n("div",null,[n("kbd",null,"G")])])],-1);kn.render=function(e,l,i,o,s,r){return a(),t("div",{class:"overlay transparent",onClick:l[2]||(l[2]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content help",onClick:l[1]||(l[1]=u((()=>{}),["stop"]))},[An,zn,Sn,Pn,In,Tn,_n,Dn,Bn,En])])};var On=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAW0lEQVQ4je1RywrAIAxLxP//5exixRWlVgZelpOKeTQFfnDypgy3eLIkSLLL8mxGPoHsU2hPAgDHBLvRX6hZZw/fwT0BtlLSONqCbWAmEIqMZOCDDlaDR3N03gOyDCn+y4DWmAAAAABJRU5ErkJggg=="}),Un=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARElEQVQ4jWNgGAU0Af+hmBCbgYGBgYERhwHEAEYGBgYGJtIdiApYyLAZBVDsAqoagC1ACQJyY4ERg0GCISh6KA4DigEAou8LC+LnIJoAAAAASUVORK5CYII="}),Nn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVQ4ja1TQQ7AIAgD///n7jCozA2Hbk00jbG1KIrcARszTugoBs49qioZj7r2kKACptkyAOCJsJuA+GzglwHjvMSSWFVaENWVASxh5eRLiq5fN/ASjI89VcP2K3hHpq1cEXNaMfnrL3TDZP2tDuoOA6MzCCXWr38AAAAASUVORK5CYII="}),Mn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAU0lEQVQ4jWNgoAH4D8X42HDARKlt5BoAd82AuQAOGLGIYQQUPv0wF5CiCQUge4EsQ5C9QI4BjMguwBYeBAElscCIy1ZivMKIwSDBEBQ9FCckigEAU3QOD7TGvY4AAAAASUVORK5CYII="});function Gn(){let e=0,t=0,n=1;const l=(l,i)=>{e+=l/n,t+=i/n},i=e=>{const t=n+.05*n*("in"===e?1:-1);return Math.min(Math.max(t,.1),6)},o=l=>({x:l.x/n-e,y:l.y/n-t}),a=l=>({x:(l.x+e)*n,y:(l.y+t)*n}),s=e=>({w:e.w*n,h:e.h*n});return{move:l,canZoom:e=>n!=i(e),zoom:(e,t)=>((e,t)=>{if(n==e)return!1;const i=1-n/e;return l(-t.x*i,-t.y*i),n=e,!0})(i(e),t),worldToViewport:e=>{const{x:t,y:n}=a(e);return{x:Math.round(t),y:Math.round(n)}},worldToViewportRaw:a,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}=o(e);return{x:Math.round(t),y:Math.round(n)}},viewportToWorldRaw:o}}function $n(e=0,t=0){const n=document.createElement("canvas");return n.width=e,n.height=t,n}var Rn={createCanvas:$n,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 l=$n(t,n);return l.getContext("2d").drawImage(e,0,0,e.width,e.height,0,0,t,n),await createImageBitmap(l)},colorizedCanvas:function(e,t,n){const l=$n(e.width,e.height),i=l.getContext("2d");return i.save(),i.drawImage(t,0,0),i.fillStyle=n,i.globalCompositeOperation="source-in",i.fillRect(0,0,t.width,t.height),i.restore(),i.save(),i.globalCompositeOperation="destination-over",i.drawImage(e,0,0),i.restore(),l}};const Vn=Re("Debug.js");let jn=0,Fn=0;var Ln=e=>{jn=performance.now(),Fn=e},Wn=e=>{const t=performance.now(),n=t-jn;n>Fn&&Vn.log(e+": "+n),jn=t};const qn=Re("PuzzleGraphics.js");function Hn(e,t){const n=We.coordByTileIdx(e,t);return{x:n.x*e.tileSize,y:n.y*e.tileSize,w:e.tileSize,h:e.tileSize}}var Qn={loadPuzzleBitmaps:async function(e){const t=await Rn.loadImageToBitmap(e.info.imageUrl),n=await Rn.resizeBitmap(t,e.info.width,e.info.height);return await async function(e,t,n){qn.log("start createPuzzleTileBitmaps");var l=n.tileSize,i=n.tileMarginWidth,o=n.tileDrawSize,a=l/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];const 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,o={x:i,y:i},r=fe.pointAdd(o,{x:l,y:0}),c=fe.pointAdd(r,{x:0,y:l}),u=fe.pointSub(c,{x:l,y:0});if(n.moveTo(o.x,o.y),0!==e.top)for(let l=0;l=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;Zn=t/2,Jn=t-Zn;const n=1/4*this.canvas.width/(t/2);Yn=-n,Kn=2*n}resize(){this.setSpeedParams()}init(){this.readyBombs=[],this.explodedBombs=[],this.particles=[];for(let e=0;e<1;e++)this.readyBombs.push(new Xn(this.rng))}update(){100*Math.random()<5&&this.readyBombs.push(new Xn(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(!h[t]){const n=e.d?a:s;if(e.color){const l=e.d?r:d;h[t]=await createImageBitmap(Rn.colorizedCanvas(n,l,e.color))}else h[t]=n}return h[t]},y=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,ol=!0})),t}(i,Rn.createCanvas()),f={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};yn.onConnectionStateChange((e=>{o.setConnectionState(e)}));const w=async e=>{f.requesting=!0;const t=await yn.requestReplayData(e,f.dataOffset,f.dataSize);return f.dataOffset+=f.dataSize,f.requesting=!1,t};let v=()=>0;const b=async()=>{if("play"===l){const l=await yn.connect(n,e,t),i=We.decodeGame(l);St.setGame(i.id,i),v=()=>D()}else{if("replay"!==l)throw"[ 2020-12-22 MODE invalid, must be play|replay ]";{const t=await w(e);if(!t.game)throw"[ 2021-05-29 no game received ]";const n=We.decodeGame(t.game);St.setGame(n.id,n),f.requesting=!1,f.log=t.log,f.lastRealTs=D(),f.gameStartTs=parseInt(f.log[0][4],10),f.lastGameTs=f.gameStartTs,v=()=>f.lastGameTs}}ol=!0};await b();const x=St.getPieceDrawOffset(e),C=St.getPieceDrawSize(e),k=St.getPuzzleWidth(e),A=St.getPuzzleHeight(e),z=St.getTableWidth(e),S=St.getTableHeight(e),P={x:(z-k)/2,y:(S-A)/2},I={w:k,h:A},T={w:C,h:C},_=await Qn.loadPuzzleBitmaps(St.getPuzzle(e)),B=new tl(y,St.getRng(e));B.init();const E=y.getContext("2d");y.classList.add("loaded");const O=Gn();O.move(-(z-y.width)/2,-(S-y.height)/2);const U=function(e,t,n){let l=[],i=!0,o=!1,a=!1,s=!1,r=!1,d=!1,c=!1,u=!1;const g=(e,t)=>{const l=n.viewportToWorld({x:e,y:t});return[l.x,l.y]},p=e=>g(e.offsetX,e.offsetY),h=()=>g(e.width/2,e.height/2),m=(e,t)=>{i&&("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?o=e:"ArrowRight"===t.key||"d"===t.key||"D"===t.key?a=e:"q"===t.key?c=e:"e"===t.key&&(d=e))};e.addEventListener("mousedown",(e=>{0===e.button&&y([Se,...p(e)])})),e.addEventListener("mouseup",(e=>{0===e.button&&y([Pe,...p(e)])})),e.addEventListener("mousemove",(e=>{y([Ie,...p(e)])})),e.addEventListener("wheel",(e=>{if(n.canZoom(e.deltaY<0?"in":"out")){const t=e.deltaY<0?Te:_e;y([t,...p(e)])}})),t.addEventListener("keydown",(e=>m(!0,e))),t.addEventListener("keyup",(e=>m(!1,e))),t.addEventListener("keypress",(e=>{i&&(" "===e.key&&y([Oe]),"F"!==e.key&&"f"!==e.key||(ll=!ll,ol=!0),"G"!==e.key&&"g"!==e.key||(il=!il,ol=!0))}));const y=e=>{l.push(e)};return{addEvent:y,consumeAll:()=>{if(0===l.length)return[];const e=l.slice();return l=[],e},createKeyEvents:()=>{const e=u?20:10,t=(o?e:0)-(a?e:0),l=(s?e:0)-(r?e:0);0===t&&0===l||y([ze,t,l]),d&&c||(d?n.canZoom("in")&&y([Te,...h()]):c&&n.canZoom("out")&&y([_e,...h()]))},setHotkeys:e=>{i=e}}}(y,window,O),N=St.getImageUrl(e),M=()=>{const t=St.getStartTs(e),n=St.getFinishTs(e),l=v();o.setFinished(!!n),o.setDuration((n||l)-t)};M(),o.setPiecesDone(St.getFinishedPiecesCount(e)),o.setPiecesTotal(St.getPieceCount(e));const G=v();o.setActivePlayers(St.getActivePlayers(e,G)),o.setIdlePlayers(St.getIdlePlayers(e,G));const $=!!St.getFinishTs(e);let R=$;const V=()=>R&&!$,j=()=>St.getPlayerBgColor(e,t)||localStorage.getItem("bg_color")||"#222222",F=()=>St.getPlayerColor(e,t)||localStorage.getItem("player_color")||"#ffffff";let L="",W="",q=!1;const H=e=>{q=e;const[t,n]=e?[L,"grab"]:[W,"default"];y.style.cursor=`url('${t}') ${u} ${p}, ${n}`},Q=e=>{L=Rn.colorizedCanvas(a,r,e).toDataURL(),W=Rn.colorizedCanvas(s,d,e).toDataURL(),H(q)};Q(F());const Y=()=>{o.setReplaySpeed&&o.setReplaySpeed(f.speeds[f.speedIdx]),o.setReplayPaused&&o.setReplayPaused(f.paused)};if("play"===l?setInterval(M,1e3):"replay"===l&&Y(),"play"===l)yn.onServerChange((n=>{n[0],n[1],n[2];const l=n[3];for(const[i,o]of l)switch(i){case Me:{const n=We.decodePlayer(o);n.id!==t&&(St.setPlayer(e,n.id,n),ol=!0)}break;case Ne:{const t=We.decodePiece(o);St.setPiece(e,t.idx,t),ol=!0}break;case Ue:St.setPuzzleData(e,o),ol=!0}R=!!St.getFinishTs(e)}));else if("replay"===l){let t=setInterval((()=>{const n=D();if(f.requesting)return void(f.lastRealTs=n);if(f.logPointer+1>=f.log.length)return f.lastRealTs=n,void(async e=>{const t=await w(e);f.log=f.log.slice(f.logPointer),f.logPointer=0,f.log.push(...t.log),t.log.length=f.log.length){f.final&&clearInterval(t);break}const l=f.log[n],o=f.gameStartTs+l[l.length-1];if(o>i)break;const a=l.slice();if(a[0]===Ce){const t=a[1];St.addPlayer(e,t,o),ol=!0}else if(a[0]===ke){const t=St.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (update player) ]";St.addPlayer(e,t,o),ol=!0}else if(a[0]===Ae){const t=St.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (handle input) ]";const n=a[2];St.handleInput(e,t,n,o),ol=!0}f.logPointer=n}f.lastRealTs=n,f.lastGameTs=i,M()}),50)}let K=null;return(e=>{const t=e.fps||60,n=e.slow||1,l=e.update,i=e.render,o=window.requestAnimationFrame,a=1/t,s=n*a;let r,d=0,c=window.performance.now();const u=()=>{for(r=window.performance.now(),d+=Math.min(1,(r-c)/1e3);d>s;)d-=s,l(a);i(d/n),c=r,o(u)};o(u)})({update:()=>{U.createKeyEvents();for(const n of U.consumeAll())if("play"===l){const l=n[0];if(l===ze){const e=n[1],t=n[2];ol=!0,O.move(e,t)}else if(l===Ie){if(K&&!St.getFirstOwnedPiece(e,t)){const e={x:n[1],y:n[2]},t=O.worldToViewport(e),l=Math.round(t.x-K.x),i=Math.round(t.y-K.y);ol=!0,O.move(l,i),K=t}}else if(l===Be)Q(n[1]);else if(l===Se){const e={x:n[1],y:n[2]};K=O.worldToViewport(e),H(!0)}else if(l===Pe)K=null,H(!1);else if(l===Te){const e={x:n[1],y:n[2]};ol=!0,O.zoom("in",O.worldToViewport(e))}else if(l===_e){const e={x:n[1],y:n[2]};ol=!0,O.zoom("out",O.worldToViewport(e))}else l===Oe&&o.togglePreview();const i=v();St.handleInput(e,t,n,i).length>0&&(ol=!0),yn.sendClientEvent(n)}else if("replay"===l){const e=n[0];if(e===ze){const e=n[1],t=n[2];ol=!0,O.move(e,t)}else if(e===Ie){if(K){const e={x:n[1],y:n[2]},t=O.worldToViewport(e),l=Math.round(t.x-K.x),i=Math.round(t.y-K.y);ol=!0,O.move(l,i),K=t}}else if(e===Se){const e={x:n[1],y:n[2]};K=O.worldToViewport(e)}else if(e===Pe)K=null;else if(e===Te){const e={x:n[1],y:n[2]};ol=!0,O.zoom("in",O.worldToViewport(e))}else if(e===_e){const e={x:n[1],y:n[2]};ol=!0,O.zoom("out",O.worldToViewport(e))}else e===Oe&&o.togglePreview()}R=!!St.getFinishTs(e),V()&&(B.update(),ol=!0)},render:async()=>{if(!ol)return;const n=v();let i,a,s;window.DEBUG&&Ln(0),E.fillStyle=j(),E.fillRect(0,0,y.width,y.height),window.DEBUG&&Wn("clear done"),i=O.worldToViewportRaw(P),a=O.worldDimToViewportRaw(I),E.fillStyle="rgba(255, 255, 255, .3)",E.fillRect(i.x,i.y,a.w,a.h),window.DEBUG&&Wn("board done");const r=St.getPiecesSortedByZIndex(e);window.DEBUG&&Wn("get tiles done"),a=O.worldDimToViewportRaw(T);for(const e of r)(-1===e.owner?ll:il)&&(s=_[e.idx],i=O.worldToViewportRaw({x:x+e.pos.x,y:x+e.pos.y}),E.drawImage(s,0,0,s.width,s.height,i.x,i.y,a.w,a.h));window.DEBUG&&Wn("tiles done");const d=[];for(const o of St.getActivePlayers(e,n))c=o,("replay"===l||c.id!==t)&&(s=await m(o),i=O.worldToViewport(o),E.drawImage(s,i.x-u,i.y-p),d.push([`${o.name} (${o.points})`,i.x,i.y+g]));var c;E.fillStyle="white",E.textAlign="center";for(const[e,t,l]of d)E.fillText(e,t,l);window.DEBUG&&Wn("players done"),o.setActivePlayers(St.getActivePlayers(e,n)),o.setIdlePlayers(St.getIdlePlayers(e,n)),o.setPiecesDone(St.getFinishedPiecesCount(e)),window.DEBUG&&Wn("HUD done"),V()&&B.render(),ol=!1}}),{setHotkeys:e=>{U.setHotkeys(e)},onBgChange:e=>{localStorage.setItem("bg_color",e),U.addEvent([De,e])},onColorChange:e=>{localStorage.setItem("player_color",e),U.addEvent([Be,e])},onNameChange:e=>{localStorage.setItem("player_name",e),U.addEvent([Ee,e])},replayOnSpeedUp:()=>{f.speedIdx+1{f.speedIdx>=1&&(f.speedIdx--,Y())},replayOnPauseToggle:()=>{f.paused=!f.paused,Y()},previewImageUrl:N,player:{background:j(),color:F(),name:St.getPlayerName(e,t)||localStorage.getItem("player_name")||"anon"},disconnect:yn.disconnect,connect:b}}var sl=e({name:"game",components:{PuzzleStatus:Jt,Scores:Ht,SettingsOverlay:en,PreviewOverlay:on,ConnectionOverlay:fn,HelpOverlay:kn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},disconnect:()=>{},connect:()=>{}}}),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.g=await al(`${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)}}))},unmounted(){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 rl={id:"game"},dl={class:"menu"},cl={class:"tabs"},ul=s("🧩 Puzzles");sl.render=function(e,i,s,r,d,c){const u=o("settings-overlay"),p=o("preview-overlay"),h=o("help-overlay"),m=o("connection-overlay"),y=o("puzzle-status"),f=o("router-link"),w=o("scores");return a(),t("div",rl,[g(n(u,{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"]),[[v,"settings"===e.overlay]]),g(n(p,{onBgclick:i[3]||(i[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[v,"preview"===e.overlay]]),g(n(h,{onBgclick:i[4]||(i[4]=t=>e.toggle("help",!0))},null,512),[[v,"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",dl,[n("div",cl,[n(f,{class:"opener",to:{name:"index"},target:"_blank"},{default:l((()=>[ul])),_:1}),n("div",{class:"opener",onClick:i[5]||(i[5]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:i[6]||(i[6]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:i[7]||(i[7]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(w,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])};var gl=e({name:"replay",components:{PuzzleStatus:Jt,Scores:Ht,SettingsOverlay:en,PreviewOverlay:on,HelpOverlay:kn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},replayOnSpeedUp:()=>{},replayOnSpeedDown:()=>{},replayOnPauseToggle:()=>{},disconnect:()=>{}},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.g=await al(`${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}}))},unmounted(){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 pl={id:"replay"},hl={class:"menu"},ml={class:"tabs"},yl=s("🧩 Puzzles");gl.render=function(e,i,s,d,c,u){const p=o("settings-overlay"),h=o("preview-overlay"),m=o("help-overlay"),y=o("puzzle-status"),f=o("router-link"),w=o("scores");return a(),t("div",pl,[g(n(p,{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"]),[[v,"settings"===e.overlay]]),g(n(h,{onBgclick:i[3]||(i[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[v,"preview"===e.overlay]]),g(n(m,{onBgclick:i[4]||(i[4]=t=>e.toggle("help",!0))},null,512),[[v,"help"===e.overlay]]),n(y,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},{default:l((()=>[n("div",null,[n("div",null,r(e.replayText),1),n("button",{class:"btn",onClick:i[5]||(i[5]=t=>e.g.replayOnSpeedUp())},"⏫"),n("button",{class:"btn",onClick:i[6]||(i[6]=t=>e.g.replayOnSpeedDown())},"⏬"),n("button",{class:"btn",onClick:i[7]||(i[7]=t=>e.g.replayOnPauseToggle())},"⏸️")])])),_:1},8,["finished","duration","piecesDone","piecesTotal"]),n("div",hl,[n("div",ml,[n(f,{class:"opener",to:{name:"index"},target:"_blank"},{default:l((()=>[yl])),_:1}),n("div",{class:"opener",onClick:i[8]||(i[8]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:i[9]||(i[9]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:i[10]||(i[10]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(w,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])},(async()=>{const e=await fetch("/api/conf"),t=await e.json();const n=b({history:x(),routes:[{name:"index",path:"/",component:R},{name:"new-game",path:"/new-game",component:Mt},{name:"game",path:"/g/:id",component:sl},{name:"replay",path:"/replay/:id",component:gl}]});n.beforeEach(((e,t)=>{t.name&&document.documentElement.classList.remove(`view-${String(t.name)}`),document.documentElement.classList.add(`view-${String(e.name)}`)}));const l=C(k);l.config.globalProperties.$config=t,l.config.globalProperties.$clientId=function(){let e=localStorage.getItem("ID");return e||(e=We.uniqId(),localStorage.setItem("ID",e)),e}(),l.use(n),l.mount("#app")})(); diff --git a/build/public/assets/index.8f906b9e.js b/build/public/assets/index.8f906b9e.js deleted file mode 100644 index df0bb1e..0000000 --- a/build/public/assets/index.8f906b9e.js +++ /dev/null @@ -1 +0,0 @@ -import{d as e,c as t,a as n,w as l,b as i,r as o,o as a,e as s,t as r,F as d,f as c,g as u,h as g,v as p,i as h,j as m,k as y,l as f,m as w,n as v,p as b,q as x,s as C}from"./vendor.b622ee49.js";var k=e({name:"app",computed:{showNav(){return!["game","replay"].includes(String(this.$route.name))}}});const A={id:"app"},T={key:0,class:"nav"},z=s("Index"),S=s("New game");k.render=function(e,s,r,d,c,u){const g=o("router-link"),p=o("router-view");return a(),t("div",A,[e.showNav?(a(),t("ul",T,[n("li",null,[n(g,{class:"btn",to:{name:"index"}},{default:l((()=>[z])),_:1})]),n("li",null,[n(g,{class:"btn",to:{name:"new-game"}},{default:l((()=>[S])),_:1})])])):i("",!0),n(p)])};const I=864e5,P=e=>{const t=Math.floor(e/I);e%=I;const n=Math.floor(e/36e5);e%=36e5;const l=Math.floor(e/6e4);e%=6e4;return`${t}d ${n}h ${l}m ${Math.floor(e/1e3)}s`};var _=1e3,D=()=>{const e=new Date;return Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())},B=(e,t)=>P(t-e),E=P,O=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?"🏁":"⏳",l=e,i=t||D();return`${n} ${B(l,i)}`}}});const U={class:"game-info-text"},M=n("br",null,null,-1),N=n("br",null,null,-1),G=n("br",null,null,-1),$=s(" β†ͺ️ Watch replay ");O.render=function(e,d,c,u,g,p){const h=o("router-link");return a(),t("div",{class:"game-teaser",style:e.style},[n(h,{class:"game-info",to:{name:"game",params:{id:e.game.id}}},{default:l((()=>[n("span",U,[s(" 🧩 "+r(e.game.tilesFinished)+"/"+r(e.game.tilesTotal),1),M,s(" πŸ‘₯ "+r(e.game.players),1),N,s(" "+r(e.time(e.game.started,e.game.finished)),1),G])])),_:1},8,["to"]),e.game.hasReplay?(a(),t(h,{key:0,class:"game-replay",to:{name:"replay",params:{id:e.game.id}}},{default:l((()=>[$])),_:1},8,["to"])):i("",!0)],4)};var R=e({components:{GameTeaser:O},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 V=n("h1",null,"Running games",-1),j=n("h1",null,"Finished games",-1);R.render=function(e,l,i,s,r,u){const g=o("game-teaser");return a(),t("div",null,[V,(a(!0),t(d,null,c(e.gamesRunning,((e,l)=>(a(),t("div",{class:"game-teaser-wrap",key:l},[n(g,{game:e},null,8,["game"])])))),128)),j,(a(!0),t(d,null,c(e.gamesFinished,((e,l)=>(a(),t("div",{class:"game-teaser-wrap",key:l},[n(g,{game:e},null,8,["game"])])))),128))])};var F=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")}}});F.render=function(e,l,i,o,s,r){return a(),t("div",{class:"imageteaser",style:e.style,onClick:l[2]||(l[2]=(...t)=>e.onClick&&e.onClick(...t))},[n("div",{class:"btn edit",onClick:l[1]||(l[1]=u(((...t)=>e.onEditClick&&e.onEditClick(...t)),["stop"]))},"✏️")],4)};var L=e({name:"image-library",components:{ImageTeaser:F},props:{images:{type:Array,required:!0}},emits:{imageClicked:null,imageEditClicked:null},methods:{imageClicked(e){this.$emit("imageClicked",e)},imageEditClicked(e){this.$emit("imageEditClicked",e)}}});L.render=function(e,n,l,i,s,r){const u=o("image-teaser");return a(),t("div",null,[(a(!0),t(d,null,c(e.images,((n,l)=>(a(),t(u,{image:n,onClick:t=>e.imageClicked(n),onEditClick:t=>e.imageEditClicked(n),key:l},null,8,["image","onClick","onEditClick"])))),128))])};const W={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}}}};W.render=function(e,n,l,i,o,s){return a(),t("div",{style:s.style,title:l.title},null,12,["title"])};var q=e({name:"tags-input",props:{modelValue:{type:Array,required:!0}},emits:{"update:modelValue":null},data:()=>({input:"",values:[]}),created(){this.values=this.modelValue},methods:{onKeyUp(e){if(","===e.key)return this.add(),e.stopPropagation(),!1},add(){const e=this.input.replace(/,/g,"").trim();e&&(this.values.includes(e)||this.values.push(e),this.input="",this.$emit("update:modelValue",this.values))},rm(e){this.values=this.values.filter((t=>t!==e)),this.$emit("update:modelValue",this.values)}}});const H=m()(((e,l,i,o,s,u)=>(a(),t("div",null,[g(n("input",{class:"input",type:"text","onUpdate:modelValue":l[1]||(l[1]=t=>e.input=t),placeholder:"Plants, People",onKeydown:l[2]||(l[2]=h(((...t)=>e.add&&e.add(...t)),["enter"])),onKeyup:l[3]||(l[3]=(...t)=>e.onKeyUp&&e.onKeyUp(...t))},null,544),[[p,e.input]]),(a(!0),t(d,null,c(e.values,((n,l)=>(a(),t("span",{key:l,class:"bit",onClick:t=>e.rm(n)},r(n)+" βœ–",9,["onClick"])))),128))]))));q.render=H,q.__scopeId="data-v-771460ae";var Q=e({name:"new-image-dialog",components:{ResponsiveImage:W,TagsInput:q},emits:{bgclick:null,setupGameClick:null,postToGalleryClick:null},data:()=>({previewUrl:"",file:null,title:"",tags:[]}),computed:{canPostToGallery(){return!(!this.previewUrl||!this.file)},canSetupGameClick(){return!(!this.previewUrl||!this.file)}},methods:{preview(e){const t=e.target;if(!t.files)return;const n=t.files[0];if(!n)return;const l=new FileReader;l.readAsDataURL(n),l.onload=e=>{this.previewUrl=e.target.result,this.file=n}},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})}}});const Y={key:0,class:"has-image"},K={key:1},Z={class:"upload"},J=n("span",{class:"btn"},"Upload File",-1),X={class:"area-settings"},ee=n("td",null,[n("label",null,"Title")],-1),te=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),le={class:"area-buttons"},ie=s("🧩 Post to gallery "),oe=n("br",null,null,-1),ae=s(" + set up game");Q.render=function(e,l,i,s,r,d){const c=o("responsive-image"),h=o("tags-input");return a(),t("div",{class:"overlay new-image-dialog",onClick:l[8]||(l[8]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:l[7]||(l[7]=u((()=>{}),["stop"]))},[n("div",{class:["area-image",{"has-image":!!e.previewUrl,"no-image":!e.previewUrl}]},[e.previewUrl?(a(),t("div",Y,[n("span",{class:"remove btn",onClick:l[1]||(l[1]=t=>e.previewUrl="")},"X"),n(c,{src:e.previewUrl},null,8,["src"])])):(a(),t("div",K,[n("label",Z,[n("input",{type:"file",style:{display:"none"},onChange:l[2]||(l[2]=(...t)=>e.preview&&e.preview(...t)),accept:"image/*"},null,32),J])]))],2),n("div",X,[n("table",null,[n("tr",null,[ee,n("td",null,[g(n("input",{type:"text","onUpdate:modelValue":l[3]||(l[3]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[p,e.title]])])]),te,n("tr",null,[ne,n("td",null,[n(h,{modelValue:e.tags,"onUpdate:modelValue":l[4]||(l[4]=t=>e.tags=t)},null,8,["modelValue"])])])])]),n("div",le,[n("button",{class:"btn",disabled:!e.canPostToGallery,onClick:l[5]||(l[5]=(...t)=>e.postToGallery&&e.postToGallery(...t))},"πŸ–ΌοΈ Post to gallery",8,["disabled"]),n("button",{class:"btn",disabled:!e.canSetupGameClick,onClick:l[6]||(l[6]=(...t)=>e.setupGameClick&&e.setupGameClick(...t))},[ie,oe,ae],8,["disabled"])])])])};var se=e({name:"edit-image-dialog",components:{ResponsiveImage:W,TagsInput:q},props:{image:{type:Object,required:!0}},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 re={class:"area-image"},de={class:"has-image"},ce={class:"area-settings"},ue=n("td",null,[n("label",null,"Title")],-1),ge=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),pe=n("td",null,[n("label",null,"Tags")],-1),he={class:"area-buttons"};function me(e,t){const n=e.x-t.x,l=e.y-t.y;return Math.sqrt(n*n+l*l)}function ye(e){return{x:e.x+e.w/2,y:e.y+e.h/2}}se.render=function(e,l,i,s,r,d){const c=o("responsive-image"),h=o("tags-input");return a(),t("div",{class:"overlay edit-image-dialog",onClick:l[5]||(l[5]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:l[4]||(l[4]=u((()=>{}),["stop"]))},[n("div",re,[n("div",de,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",ce,[n("table",null,[n("tr",null,[ue,n("td",null,[g(n("input",{type:"text","onUpdate:modelValue":l[1]||(l[1]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[p,e.title]])])]),ge,n("tr",null,[pe,n("td",null,[n(h,{modelValue:e.tags,"onUpdate:modelValue":l[2]||(l[2]=t=>e.tags=t)},null,8,["modelValue"])])])])]),n("div",he,[n("button",{class:"btn",onClick:l[3]||(l[3]=(...t)=>e.saveImage&&e.saveImage(...t))},"πŸ–ΌοΈ Save image")])])])};var fe={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:me,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:ye,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 me(ye(e),ye(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)}};var we=1,ve=4,be=2,xe=3,Ce=2,ke=4,Ae=3,Te=9,ze=1,Se=2,Ie=3,Pe=4,_e=5,De=6,Be=7,Ee=8,Oe=10,Ue=1,Me=2,Ne=3;class Ge{constructor(e){this.rand_high=e||3735929054,this.rand_low=1231121986^e}random(e,t){return 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,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),l=t[n];t[n]=t[e],t[e]=l}return t}static serialize(e){return{rand_high:e.rand_high,rand_low:e.rand_low}}static unserialize(e){const t=new Ge(0);return t.rand_high=e.rand_high,t.rand_low=e.rand_low,t}}const $e=(e,t)=>{const n=`${e}`;return n.length>=t.length?n:t.substr(0,t.length-n.length)+n},Re=(...e)=>{const t=t=>(...n)=>{const l=new Date,i=$e(l.getHours(),"00"),o=$e(l.getMinutes(),"00"),a=$e(l.getSeconds(),"00");console[t](`${i}:${o}:${a}`,...e,...n)};return{log:t("log"),error:t("error"),info:t("info")}};var Ve,je,Fe,Le,We={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}},encodeTile:function(e){return[e.idx,e.pos.x,e.pos.y,e.z,e.owner,e.group]},decodeTile: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 Array.isArray(e)?e:[e.id,e.rng.type,Ge.serialize(e.rng.obj),e.puzzle,e.players,e.evtInfos,e.scoreMode]},decodeGame:function(e){return Array.isArray(e)?{id:e[0],rng:{type:e[1],obj:Ge.unserialize(e[2])},puzzle:e[3],players:e[4],evtInfos:e[5],scoreMode:e[6]}:e},coordByTileIdx:function(e,t){const n=e.width/e.tileSize;return{x:t%n,y:Math.floor(t/n)}},asQueryArgs:function(e){const t=[];for(let n in e){const l=[n,e[n]].map(encodeURIComponent);t.push(l.join("="))}return 0===t.length?"":`?${t.join("&")}`}};(je=Ve||(Ve={}))[je.Flat=0]="Flat",je[je.Out=1]="Out",je[je.In=-1]="In",(Le=Fe||(Fe={}))[Le.FINAL=0]="FINAL",Le[Le.ANY=1]="ANY";const qe={};function He(e,t){return{id:e,x:0,y:0,d:0,name:null,color:null,bgcolor:null,points:0,ts:t}}function Qe(e,t){let n=0;for(let l of qe[e].players){if(We.decodePlayer(l).id===t)return n;n++}return-1}function Ye(e,t){const n=Qe(e,t);return-1===n?null:We.decodePlayer(qe[e].players[n])}function Ke(e,t,n){const l=Qe(e,t);-1===l?qe[e].players.push(We.encodePlayer(n)):qe[e].players[l]=We.encodePlayer(n)}function Ze(e,t){return-1!==Qe(e,t)}function Je(e){return qe[e]?qe[e].players.map(We.decodePlayer):[]}function Xe(e){return qe[e].puzzle.tiles.length}function et(e){return qe[e].scoreMode||0}function tt(e){return nt(e)===Xe(e)}function nt(e){let t=0;for(let n of qe[e].puzzle.tiles)-1===We.decodeTile(n).owner&&t++;return t}function lt(e,t,n){const l=Ye(e,t);if(null!==l){for(let e of Object.keys(n))l[e]=n[e];Ke(e,t,l)}}function it(e,t){for(let n of Object.keys(t))qe[e].puzzle.data[n]=t[n]}function ot(e,t,n){for(let l of Object.keys(n)){const i=We.decodeTile(qe[e].puzzle.tiles[t]);i[l]=n[l],qe[e].puzzle.tiles[t]=We.encodeTile(i)}}const at=(e,t)=>We.decodeTile(qe[e].puzzle.tiles[t]),st=(e,t)=>at(e,t).group,rt=(e,t)=>{const n=qe[e].puzzle.info,l={x:(n.table.width-n.width)/2,y:(n.table.height-n.height)/2},i=function(e,t){const n=qe[e].puzzle.info,l=We.coordByTileIdx(n,t),i=l.x*n.tileSize,o=l.y*n.tileSize;return{x:i,y:o}}(e,t);return fe.pointAdd(l,i)},dt=(e,t)=>at(e,t).pos,ct=e=>{const t=Tt(e),n=zt(e),l=Math.round(t/4),i=Math.round(n/4);return{x:0-l,y:0-i,w:t+2*l,h:n+2*i}},ut=(e,t)=>{const n=mt(e),l=at(e,t);return{x:l.pos.x,y:l.pos.y,w:n,h:n}},gt=(e,t)=>at(e,t).z,pt=(e,t)=>{for(let n of qe[e].puzzle.tiles){const e=We.decodeTile(n);if(e.owner===t)return e.idx}return-1},ht=e=>qe[e].puzzle.info.tileDrawSize,mt=e=>qe[e].puzzle.info.tileSize,yt=e=>qe[e].puzzle.data.maxGroup,ft=e=>qe[e].puzzle.data.maxZ;function wt(e,t){const n=qe[e].puzzle.info,l=We.coordByTileIdx(n,t);return[l.y>0?t-n.tilesX:-1,l.x0?t-1:-1]}const vt=(e,t,n)=>{for(let l of t)ot(e,l,{z:n})},bt=(e,t,n)=>{const l=dt(e,t);ot(e,t,{pos:fe.pointAdd(l,n)})},xt=(e,t,n)=>{const l=ht(e),i=ct(e),o=n;for(let a of t){const t=at(e,a);t.pos.x+n.xi.x+i.w&&(o.x=Math.min(i.x+i.w-t.pos.x+l,o.x)),t.pos.y+n.yi.y+i.h&&(o.y=Math.min(i.y+i.h-t.pos.y+l,o.y))}for(let a of t)bt(e,a,o)},Ct=(e,t,n)=>{for(let l of t)ot(e,l,{owner:n})};function kt(e,t){const n=qe[e].puzzle.tiles,l=We.decodeTile(n[t]),i=[];if(l.group)for(let o of n){const e=We.decodeTile(o);e.group===l.group&&i.push(e.idx)}else i.push(l.idx);return i}const At=(e,t)=>{const n=Ye(e,t);return n?n.points:0},Tt=e=>qe[e].puzzle.info.table.width,zt=e=>qe[e].puzzle.info.table.height;var St={__createPlayerObject:He,setGame:function(e,t){qe[e]=t},exists:function(e){return!!qe[e]||!1},playerExists:Ze,getActivePlayers:function(e,t){const n=t-30*_;return Je(e).filter((e=>e.ts>=n))},getIdlePlayers:function(e,t){const n=t-30*_;return Je(e).filter((e=>e.ts0))},addPlayer:function(e,t,n){Ze(e,t)?lt(e,t,{ts:n}):Ke(e,t,He(t,n))},getFinishedTileCount:nt,getTileCount:Xe,getImageUrl:function(e){return qe[e].puzzle.info.imageUrl},setImageUrl:function(e,t){qe[e].puzzle.info.imageUrl=t},get:function(e){return qe[e]},getAllGames:function(){return Object.values(qe).sort(((e,t)=>tt(e.id)===tt(t.id)?t.puzzle.data.started-e.puzzle.data.started:tt(e.id)?1:-1))},getPlayerBgColor:(e,t)=>{const n=Ye(e,t);return n?n.bgcolor:null},getPlayerColor:(e,t)=>{const n=Ye(e,t);return n?n.color:null},getPlayerName:(e,t)=>{const n=Ye(e,t);return n?n.name:null},getPlayerIndexById:Qe,getPlayerIdByIndex:function(e,t){return qe[e].players.length>t?We.decodePlayer(qe[e].players[t]).id:null},changePlayer:lt,setPlayer:Ke,setTile:function(e,t,n){qe[e].puzzle.tiles[t]=We.encodeTile(n)},setPuzzleData:function(e,t){qe[e].puzzle.data=t},getTableWidth:Tt,getTableHeight:zt,getPuzzle:e=>qe[e].puzzle,getRng:e=>qe[e].rng.obj,getPuzzleWidth:e=>qe[e].puzzle.info.width,getPuzzleHeight:e=>qe[e].puzzle.info.height,getTilesSortedByZIndex:function(e){return qe[e].puzzle.tiles.map(We.decodeTile).sort(((e,t)=>e.z-t.z))},getFirstOwnedTile:(e,t)=>{const n=pt(e,t);return n<0?null:qe[e].puzzle.tiles[n]},getTileDrawOffset:e=>qe[e].puzzle.info.tileDrawOffset,getTileDrawSize:ht,getFinalTilePos:rt,getStartTs:e=>qe[e].puzzle.data.started,getFinishTs:e=>qe[e].puzzle.data.finished,handleInput:function(e,t,n,l){const i=qe[e].puzzle,o=function(e,t){return t in qe[e].evtInfos?qe[e].evtInfos[t]:{_last_mouse:null,_last_mouse_down:null}}(e,t),a=[],s=()=>{a.push([Ue,i.data])},r=t=>{a.push([Me,We.encodeTile(at(e,t))])},d=e=>{for(const t of e)r(t)},c=()=>{const n=Ye(e,t);n&&a.push([Ne,We.encodePlayer(n)])},u=n[0];if(u===De){const i=n[1];lt(e,t,{bgcolor:i,ts:l}),c()}else if(u===Be){const i=n[1];lt(e,t,{color:i,ts:l}),c()}else if(u===Ee){const i=`${n[1]}`.substr(0,16);lt(e,t,{name:i,ts:l}),c()}else if(u===ze){const i={x:n[1],y:n[2]};lt(e,t,{d:1,ts:l}),c(),o._last_mouse_down=i;const a=((e,t)=>{let n=qe[e].puzzle.info,l=qe[e].puzzle.tiles,i=-1,o=-1;for(let a=0;ai)&&(i=e.z,o=a)}return o})(e,i);if(a>=0){let n=ft(e)+1;it(e,{maxZ:n}),s();const l=kt(e,a);vt(e,l,ft(e)),Ct(e,l,t),d(l)}o._last_mouse=i}else if(u===Ie){const i=n[1],a=n[2],s={x:i,y:a};if(null===o._last_mouse_down)lt(e,t,{x:i,y:a,ts:l}),c();else{let n=pt(e,t);if(n>=0){lt(e,t,{x:i,y:a,ts:l}),c();const r=kt(e,n);let u=fe.pointInBounds(s,ct(e))&&fe.pointInBounds(o._last_mouse_down,ct(e));for(let t of r){const n=ut(e,t);if(fe.pointInBounds(s,n)){u=!0;break}}if(u){const t=i-o._last_mouse_down.x,n=a-o._last_mouse_down.y;xt(e,r,{x:t,y:n}),d(r)}}else lt(e,t,{ts:l}),c();o._last_mouse_down=s}o._last_mouse=s}else if(u===Se){const a={x:n[1],y:n[2]},u=0;o._last_mouse_down=null;let g=pt(e,t);if(g>=0){let n=kt(e,g);Ct(e,n,0),d(n);let o=dt(e,g),a=rt(e,g);if(fe.pointDistance(a,o){for(let n of t)ot(e,n,{owner:-1,z:1})})(e,n),d(n);let r=At(e,t);0===et(e)?r+=n.length:1===et(e)&&(r+=1),lt(e,t,{d:u,ts:l,points:r}),c(),nt(e)===Xe(e)&&(it(e,{finished:l}),s())}else{const n=(e,t,n,l)=>{let i=qe[e].puzzle.info;if(n<0)return!1;if(((e,t,n)=>{const l=st(e,t),i=st(e,n);return!(!l||l!==i)})(e,t,n))return!1;const o=dt(e,t),a=fe.pointAdd(dt(e,n),{x:l[0]*i.tileSize,y:l[1]*i.tileSize});if(fe.pointDistance(o,a){const l=qe[e].puzzle.tiles,i=st(e,t),o=st(e,n);let a;const d=[];i&&d.push(i),o&&d.push(o),i?a=i:o?a=o:(it(e,{maxGroup:yt(e)+1}),s(),a=yt(e));if(ot(e,t,{group:a}),r(t),ot(e,n,{group:a}),r(n),d.length>0)for(const s of l){const t=We.decodeTile(s);d.includes(t.group)&&(ot(e,t.idx,{group:a}),r(t.idx))}})(e,t,n),i=kt(e,t);const c=((e,t)=>{let n=0;for(let l of t){let t=gt(e,l);t>n&&(n=t)}return n})(e,i);return vt(e,i,c),d(i),!0}return!1};let i=!1;for(let t of kt(e,g)){let l=wt(e,t);if(n(e,t,l[0],[0,1])||n(e,t,l[1],[-1,0])||n(e,t,l[2],[0,-1])||n(e,t,l[3],[1,0])){i=!0;break}}if(i&&1===et(e)){const n=At(e,t)+1;lt(e,t,{d:u,ts:l,points:n}),c()}else lt(e,t,{d:u,ts:l}),c()}}else lt(e,t,{d:u,ts:l}),c();o._last_mouse=a}else if(u===Pe){const i=n[1],a=n[2];lt(e,t,{x:i,y:a,ts:l}),c(),o._last_mouse={x:i,y:a}}else if(u===_e){const i=n[1],a=n[2];lt(e,t,{x:i,y:a,ts:l}),c(),o._last_mouse={x:i,y:a}}else lt(e,t,{ts:l}),c();return function(e,t,n){qe[e].evtInfos[t]=n}(e,t,o),a}},It=e({name:"new-game-dialog",components:{ResponsiveImage:W},props:{image:{type:Object,required:!0}},emits:{newGame:null,bgclick:null},data:()=>({tiles:1e3,scoreMode:Fe.ANY}),methods:{onNewGameClick(){this.$emit("newGame",{tiles:this.tilesInt,image:this.image,scoreMode:this.scoreModeInt})}},computed:{canStartNewGame(){return!!(this.tilesInt&&this.image&&this.image.url&&[0,1].includes(this.scoreModeInt))},scoreModeInt(){return parseInt(`${this.scoreMode}`,10)},tilesInt(){return parseInt(`${this.tiles}`,10)}}});const Pt={class:"area-image"},_t={class:"has-image"},Dt={class:"area-settings"},Bt=n("td",null,[n("label",null,"Pieces")],-1),Et=n("td",null,[n("label",null,"Scoring: ")],-1),Ot=s(" Any (Score when pieces are connected to each other or on final location)"),Ut=n("br",null,null,-1),Mt=s(" Final (Score when pieces are put to their final location)"),Nt={class:"area-buttons"};It.render=function(e,l,i,s,r,d){const c=o("responsive-image");return a(),t("div",{class:"overlay new-game-dialog",onClick:l[6]||(l[6]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:l[5]||(l[5]=u((()=>{}),["stop"]))},[n("div",Pt,[n("div",_t,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",Dt,[n("table",null,[n("tr",null,[Bt,n("td",null,[g(n("input",{type:"text","onUpdate:modelValue":l[1]||(l[1]=t=>e.tiles=t)},null,512),[[p,e.tiles]])])]),n("tr",null,[Et,n("td",null,[n("label",null,[g(n("input",{type:"radio","onUpdate:modelValue":l[2]||(l[2]=t=>e.scoreMode=t),value:"1"},null,512),[[y,e.scoreMode]]),Ot]),Ut,n("label",null,[g(n("input",{type:"radio","onUpdate:modelValue":l[3]||(l[3]=t=>e.scoreMode=t),value:"0"},null,512),[[y,e.scoreMode]]),Mt])])])])]),n("div",Nt,[n("button",{class:"btn",disabled:!e.canStartNewGame,onClick:l[4]||(l[4]=(...t)=>e.onNewGameClick&&e.onNewGameClick(...t))}," 🧩 Generate Puzzle ",8,["disabled"])])])])};var Gt=e({components:{ImageLibrary:L,NewImageDialog:Q,EditImageDialog:se,NewGameDialog:It},data:()=>({filters:{sort:"date_desc",tags:[]},images:[],tags:[],image:{id:0,filename:"",file:"",url:"",title:"",tags:[],created:0},dialog:""}),async created(){await this.loadImages()},methods:{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${We.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("/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 $t={class:"upload-image-teaser"},Rt=n("div",{class:"hint"},"(The image you upload will be added to the public gallery.)",-1),Vt={key:0},jt=s(" Tags: "),Ft=s(" Sort by: "),Lt=n("option",{value:"date_desc"},"Newest first",-1),Wt=n("option",{value:"date_asc"},"Oldest first",-1),qt=n("option",{value:"alpha_asc"},"A-Z",-1),Ht=n("option",{value:"alpha_desc"},"Z-A",-1);Gt.render=function(e,l,s,u,p,h){const m=o("image-library"),y=o("new-image-dialog"),w=o("edit-image-dialog"),v=o("new-game-dialog");return a(),t("div",null,[n("div",$t,[n("div",{class:"btn btn-big",onClick:l[1]||(l[1]=t=>e.dialog="new-image")},"Upload your image"),Rt]),n("div",null,[e.tags.length>0?(a(),t("label",Vt,[jt,(a(!0),t(d,null,c(e.tags,((n,l)=>(a(),t("span",{class:["bit",{on:e.filters.tags.includes(n.slug)}],key:l,onClick:t=>e.toggleTag(n)},r(n.title),11,["onClick"])))),128))])):i("",!0),n("label",null,[Ft,g(n("select",{"onUpdate:modelValue":l[2]||(l[2]=t=>e.filters.sort=t),onChange:l[3]||(l[3]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[Lt,Wt,qt,Ht],544),[[f,e.filters.sort]])])]),n(m,{images:e.images,onImageClicked:e.onImageClicked,onImageEditClicked:e.onImageEditClicked},null,8,["images","onImageClicked","onImageEditClicked"]),"new-image"===e.dialog?(a(),t(y,{key:0,onBgclick:l[4]||(l[4]=t=>e.dialog=""),onPostToGalleryClick:e.postToGalleryClick,onSetupGameClick:e.setupGameClick},null,8,["onPostToGalleryClick","onSetupGameClick"])):i("",!0),"edit-image"===e.dialog?(a(),t(w,{key:1,onBgclick:l[5]||(l[5]=t=>e.dialog=""),onSaveClick:e.onSaveImageClick,image:e.image},null,8,["onSaveClick","image"])):i("",!0),e.image&&"new-game"===e.dialog?(a(),t(v,{key:2,onBgclick:l[6]||(l[6]=t=>e.dialog=""),onNewGame:e.onNewGame,image:e.image},null,8,["onNewGame","image"])):i("",!0)])};var Qt=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 Yt={class:"scores"},Kt=n("div",null,"Scores",-1),Zt=n("td",null,"⚑",-1),Jt=n("td",null,"πŸ’€",-1);Qt.render=function(e,l,i,o,s,u){return a(),t("div",Yt,[Kt,n("table",null,[(a(!0),t(d,null,c(e.actives,((e,l)=>(a(),t("tr",{key:l,style:{color:e.color}},[Zt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128)),(a(!0),t(d,null,c(e.idles,((e,l)=>(a(),t("tr",{key:l,style:{color:e.color}},[Jt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128))])])};var Xt=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 E(this.duration)}}});const en={class:"timer"};Xt.render=function(e,l,i,o,s,d){return a(),t("div",en,[n("div",null," 🧩 "+r(e.piecesDone)+"/"+r(e.piecesTotal),1),n("div",null,r(e.icon)+" "+r(e.durationStr),1),w(e.$slots,"default")])};var tn=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 nn=n("td",null,[n("label",null,"Background: ")],-1),ln=n("td",null,[n("label",null,"Color: ")],-1),on=n("td",null,[n("label",null,"Name: ")],-1);tn.render=function(e,l,i,o,s,r){return a(),t("div",{class:"overlay transparent",onClick:l[5]||(l[5]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content settings",onClick:l[4]||(l[4]=u((()=>{}),["stop"]))},[n("tr",null,[nn,n("td",null,[g(n("input",{type:"color","onUpdate:modelValue":l[1]||(l[1]=t=>e.modelValue.background=t)},null,512),[[p,e.modelValue.background]])])]),n("tr",null,[ln,n("td",null,[g(n("input",{type:"color","onUpdate:modelValue":l[2]||(l[2]=t=>e.modelValue.color=t)},null,512),[[p,e.modelValue.color]])])]),n("tr",null,[on,n("td",null,[g(n("input",{type:"text",maxLength:"16","onUpdate:modelValue":l[3]||(l[3]=t=>e.modelValue.name=t)},null,512),[[p,e.modelValue.name]])])])])])};var an=e({name:"preview-overlay",props:{img:String},emits:{bgclick:null},computed:{previewStyle(){return{backgroundImage:`url('${this.img}')`}}}});const sn={class:"preview"};an.render=function(e,l,i,o,s,r){return a(),t("div",{class:"overlay",onClick:l[1]||(l[1]=t=>e.$emit("bgclick"))},[n("div",sn,[n("div",{class:"img",style:e.previewStyle},null,4)])])};const rn=Re("Communication.js");let dn,cn=e=>{},un=e=>{};let gn=0;const pn=e=>{gn!==e&&(gn=e,un(e))};function hn(e){if(2===gn)try{dn.send(JSON.stringify(e))}catch(t){rn.info("unable to send message.. maybe because ws is invalid?")}}let mn,yn;var fn={connect:function(e,t,n){return mn=0,yn={},pn(3),new Promise((l=>{dn=new WebSocket(e,n+"|"+t),dn.onopen=e=>{pn(2),hn([xe])},dn.onmessage=e=>{const t=JSON.parse(e.data),i=t[0];if(i===ve){const e=t[1];l(e)}else{if(i!==we)throw`[ 2021-05-09 invalid connect msgType ${i} ]`;{const e=t[1],l=t[2];if(e===n&&yn[l])return void delete yn[l];cn(t)}}},dn.onerror=e=>{throw pn(1),"[ 2021-05-15 onerror ]"},dn.onclose=e=>{4e3===e.code||1001===e.code?pn(4):pn(1)}}))},requestReplayData:async function(e,t,n){const l=await fetch(`/api/replay-data?gameId=${e}&offset=${t}&size=${n}`);return await l.json()},disconnect:function(){dn&&dn.close(4e3),mn=0,yn={}},sendClientEvent:function(e){mn++,yn[mn]=e,hn([be,mn,yn[mn]])},onServerChange:function(e){cn=e},onConnectionStateChange:function(e){un=e},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},wn=e({name:"connection-overlay",emits:{reconnect:null},props:{connectionState:Number},computed:{lostConnection(){return this.connectionState===fn.CONN_STATE_DISCONNECTED},connecting(){return this.connectionState===fn.CONN_STATE_CONNECTING},show(){return!(!this.lostConnection&&!this.connecting)}}});const vn={key:0,class:"overlay connection-lost"},bn={key:0,class:"overlay-content"},xn=n("div",null,"⁉️ LOST CONNECTION ⁉️",-1),Cn={key:1,class:"overlay-content"},kn=n("div",null,"Connecting...",-1);wn.render=function(e,l,o,s,r,d){return e.show?(a(),t("div",vn,[e.lostConnection?(a(),t("div",bn,[xn,n("span",{class:"btn",onClick:l[1]||(l[1]=t=>e.$emit("reconnect"))},"Reconnect")])):i("",!0),e.connecting?(a(),t("div",Cn,[kn])):i("",!0)])):i("",!0)};var An=e({name:"help-overlay",emits:{bgclick:null}});const Tn=n("tr",null,[n("td",null,"⬆️ Move up:"),n("td",null,[n("div",null,[n("kbd",null,"W"),s("/"),n("kbd",null,"↑"),s("/πŸ–±οΈ")])])],-1),zn=n("tr",null,[n("td",null,"⬇️ Move down:"),n("td",null,[n("div",null,[n("kbd",null,"S"),s("/"),n("kbd",null,"↓"),s("/πŸ–±οΈ")])])],-1),Sn=n("tr",null,[n("td",null,"⬅️ Move left:"),n("td",null,[n("div",null,[n("kbd",null,"A"),s("/"),n("kbd",null,"←"),s("/πŸ–±οΈ")])])],-1),In=n("tr",null,[n("td",null,"➑️ Move right:"),n("td",null,[n("div",null,[n("kbd",null,"D"),s("/"),n("kbd",null,"β†’"),s("/πŸ–±οΈ")])])],-1),Pn=n("tr",null,[n("td"),n("td",null,[n("div",null,[s("Move faster by holding "),n("kbd",null,"Shift")])])],-1),_n=n("tr",null,[n("td",null,"πŸ”+ Zoom in:"),n("td",null,[n("div",null,[n("kbd",null,"E"),s("/πŸ–±οΈ-Wheel")])])],-1),Dn=n("tr",null,[n("td",null,"πŸ”- Zoom out:"),n("td",null,[n("div",null,[n("kbd",null,"Q"),s("/πŸ–±οΈ-Wheel")])])],-1),Bn=n("tr",null,[n("td",null,"πŸ–ΌοΈ Toggle preview:"),n("td",null,[n("div",null,[n("kbd",null,"Space")])])],-1),En=n("tr",null,[n("td",null,"πŸ§©βœ”οΈ Toggle fixed pieces:"),n("td",null,[n("div",null,[n("kbd",null,"F")])])],-1),On=n("tr",null,[n("td",null,"πŸ§©β“ Toggle loose pieces:"),n("td",null,[n("div",null,[n("kbd",null,"G")])])],-1);An.render=function(e,l,i,o,s,r){return a(),t("div",{class:"overlay transparent",onClick:l[2]||(l[2]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content help",onClick:l[1]||(l[1]=u((()=>{}),["stop"]))},[Tn,zn,Sn,In,Pn,_n,Dn,Bn,En,On])])};var Un=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAW0lEQVQ4je1RywrAIAxLxP//5exixRWlVgZelpOKeTQFfnDypgy3eLIkSLLL8mxGPoHsU2hPAgDHBLvRX6hZZw/fwT0BtlLSONqCbWAmEIqMZOCDDlaDR3N03gOyDCn+y4DWmAAAAABJRU5ErkJggg=="}),Mn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARElEQVQ4jWNgGAU0Af+hmBCbgYGBgYERhwHEAEYGBgYGJtIdiApYyLAZBVDsAqoagC1ACQJyY4ERg0GCISh6KA4DigEAou8LC+LnIJoAAAAASUVORK5CYII="}),Nn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVQ4ja1TQQ7AIAgD///n7jCozA2Hbk00jbG1KIrcARszTugoBs49qioZj7r2kKACptkyAOCJsJuA+GzglwHjvMSSWFVaENWVASxh5eRLiq5fN/ASjI89VcP2K3hHpq1cEXNaMfnrL3TDZP2tDuoOA6MzCCXWr38AAAAASUVORK5CYII="}),Gn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAU0lEQVQ4jWNgoAH4D8X42HDARKlt5BoAd82AuQAOGLGIYQQUPv0wF5CiCQUge4EsQ5C9QI4BjMguwBYeBAElscCIy1ZivMKIwSDBEBQ9FCckigEAU3QOD7TGvY4AAAAASUVORK5CYII="});function $n(){let e=0,t=0,n=1;const l=(l,i)=>{e+=l/n,t+=i/n},i=e=>{const t=n+.05*n*("in"===e?1:-1);return Math.min(Math.max(t,.1),6)},o=l=>({x:l.x/n-e,y:l.y/n-t}),a=l=>({x:(l.x+e)*n,y:(l.y+t)*n}),s=e=>({w:e.w*n,h:e.h*n});return{move:l,canZoom:e=>n!=i(e),zoom:(e,t)=>((e,t)=>{if(n==e)return!1;const i=1-n/e;return l(-t.x*i,-t.y*i),n=e,!0})(i(e),t),worldToViewport:e=>{const{x:t,y:n}=a(e);return{x:Math.round(t),y:Math.round(n)}},worldToViewportRaw:a,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}=o(e);return{x:Math.round(t),y:Math.round(n)}},viewportToWorldRaw:o}}function Rn(e=0,t=0){const n=document.createElement("canvas");return n.width=e,n.height=t,n}var Vn={createCanvas:Rn,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 l=Rn(t,n);return l.getContext("2d").drawImage(e,0,0,e.width,e.height,0,0,t,n),await createImageBitmap(l)},colorizedCanvas:function(e,t,n){const l=Rn(e.width,e.height),i=l.getContext("2d");return i.save(),i.drawImage(t,0,0),i.fillStyle=n,i.globalCompositeOperation="source-in",i.fillRect(0,0,t.width,t.height),i.restore(),i.save(),i.globalCompositeOperation="destination-over",i.drawImage(e,0,0),i.restore(),l}};const jn=Re("Debug.js");let Fn=0,Ln=0;var Wn=e=>{Fn=performance.now(),Ln=e},qn=e=>{const t=performance.now(),n=t-Fn;n>Ln&&jn.log(e+": "+n),Fn=t};const Hn=Re("PuzzleGraphics.js");function Qn(e,t){const n=We.coordByTileIdx(e,t);return{x:n.x*e.tileSize,y:n.y*e.tileSize,w:e.tileSize,h:e.tileSize}}var Yn={loadPuzzleBitmaps:async function(e){const t=await Vn.loadImageToBitmap(e.info.imageUrl),n=await Vn.resizeBitmap(t,e.info.width,e.info.height);return await async function(e,t,n){Hn.log("start createPuzzleTileBitmaps");var l=n.tileSize,i=n.tileMarginWidth,o=n.tileDrawSize,a=l/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];const 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,o={x:i,y:i},r=fe.pointAdd(o,{x:l,y:0}),c=fe.pointAdd(r,{x:0,y:l}),u=fe.pointSub(c,{x:l,y:0});if(n.moveTo(o.x,o.y),0!==e.top)for(let l=0;l=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;Jn=t/2,Xn=t-Jn;const n=1/4*this.canvas.width/(t/2);Kn=-n,Zn=2*n}resize(){this.setSpeedParams()}init(){this.readyBombs=[],this.explodedBombs=[],this.particles=[];for(let e=0;e<1;e++)this.readyBombs.push(new el(this.rng))}update(){100*Math.random()<5&&this.readyBombs.push(new el(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(!h[t]){const n=e.d?a:s;if(e.color){const l=e.d?r:d;h[t]=await createImageBitmap(Vn.colorizedCanvas(n,l,e.color))}else h[t]=n}return h[t]},y=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,al=!0})),t}(i,Vn.createCanvas()),f={final:!1,requesting:!0,log:[],logPointer:0,logIdx:0,speeds:[.5,1,2,5,10,20,50,100,250,500],speedIdx:1,paused:!1,lastRealTs:0,lastGameTs:0,gameStartTs:0};fn.onConnectionStateChange((e=>{o.setConnectionState(e)}));let w=()=>0;const v=async()=>{if("play"===l){const l=await fn.connect(n,e,t),i=We.decodeGame(l);St.setGame(i.id,i),w=()=>D()}else{if("replay"!==l)throw"[ 2020-12-22 MODE invalid, must be play|replay ]";{const t=await fn.requestReplayData(e,0,1e4),n=We.decodeGame(t.game);St.setGame(n.id,n),f.requesting=!1,f.log=t.log,f.lastRealTs=D(),f.gameStartTs=parseInt(f.log[0][4],10),f.lastGameTs=f.gameStartTs,w=()=>f.lastGameTs}}al=!0};await v();const b=St.getTileDrawOffset(e),x=St.getTileDrawSize(e),C=St.getPuzzleWidth(e),k=St.getPuzzleHeight(e),A=St.getTableWidth(e),T=St.getTableHeight(e),z={x:(A-C)/2,y:(T-k)/2},S={w:C,h:k},I={w:x,h:x},P=await Yn.loadPuzzleBitmaps(St.getPuzzle(e)),_=new nl(y,St.getRng(e));_.init();const B=y.getContext("2d");y.classList.add("loaded");const E=$n();E.move(-(A-y.width)/2,-(T-y.height)/2);const O=function(e,t,n){let l=[],i=!0,o=!1,a=!1,s=!1,r=!1,d=!1,c=!1,u=!1;const g=(e,t)=>{const l=n.viewportToWorld({x:e,y:t});return[l.x,l.y]},p=e=>g(e.offsetX,e.offsetY),h=()=>g(e.width/2,e.height/2),m=(e,t)=>{i&&("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?o=e:"ArrowRight"===t.key||"d"===t.key||"D"===t.key?a=e:"q"===t.key?c=e:"e"===t.key&&(d=e))};e.addEventListener("mousedown",(e=>{0===e.button&&y([ze,...p(e)])})),e.addEventListener("mouseup",(e=>{0===e.button&&y([Se,...p(e)])})),e.addEventListener("mousemove",(e=>{y([Ie,...p(e)])})),e.addEventListener("wheel",(e=>{if(n.canZoom(e.deltaY<0?"in":"out")){const t=e.deltaY<0?Pe:_e;y([t,...p(e)])}})),t.addEventListener("keydown",(e=>m(!0,e))),t.addEventListener("keyup",(e=>m(!1,e))),t.addEventListener("keypress",(e=>{i&&(" "===e.key&&y([Oe]),"F"!==e.key&&"f"!==e.key||(il=!il,al=!0),"G"!==e.key&&"g"!==e.key||(ol=!ol,al=!0))}));const y=e=>{l.push(e)};return{addEvent:y,consumeAll:()=>{if(0===l.length)return[];const e=l.slice();return l=[],e},createKeyEvents:()=>{const e=u?20:10,t=(o?e:0)-(a?e:0),l=(s?e:0)-(r?e:0);0===t&&0===l||y([Te,t,l]),d&&c||(d?n.canZoom("in")&&y([Pe,...h()]):c&&n.canZoom("out")&&y([_e,...h()]))},setHotkeys:e=>{i=e}}}(y,window,E),U=St.getImageUrl(e),M=()=>{const t=St.getStartTs(e),n=St.getFinishTs(e),l=w();o.setFinished(!!n),o.setDuration((n||l)-t)};M(),o.setPiecesDone(St.getFinishedTileCount(e)),o.setPiecesTotal(St.getTileCount(e));const N=w();o.setActivePlayers(St.getActivePlayers(e,N)),o.setIdlePlayers(St.getIdlePlayers(e,N));const G=!!St.getFinishTs(e);let $=G;const R=()=>$&&!G,V=()=>St.getPlayerBgColor(e,t)||localStorage.getItem("bg_color")||"#222222",j=()=>St.getPlayerColor(e,t)||localStorage.getItem("player_color")||"#ffffff";let F="",L="",W=!1;const q=e=>{W=e;const[t,n]=e?[F,"grab"]:[L,"default"];y.style.cursor=`url('${t}') ${u} ${p}, ${n}`},H=e=>{F=Vn.colorizedCanvas(a,r,e).toDataURL(),L=Vn.colorizedCanvas(s,d,e).toDataURL(),q(W)};H(j());const Q=()=>{o.setReplaySpeed&&o.setReplaySpeed(f.speeds[f.speedIdx]),o.setReplayPaused&&o.setReplayPaused(f.paused)};if("play"===l?setInterval(M,1e3):"replay"===l&&Q(),"play"===l)fn.onServerChange((n=>{n[0],n[1],n[2];const l=n[3];for(const[i,o]of l)switch(i){case Ne:{const n=We.decodePlayer(o);n.id!==t&&(St.setPlayer(e,n.id,n),al=!0)}break;case Me:{const t=We.decodeTile(o);St.setTile(e,t.idx,t),al=!0}break;case Ue:St.setPuzzleData(e,o),al=!0}$=!!St.getFinishTs(e)}));else if("replay"===l){let t=setInterval((()=>{const n=D();if(f.requesting)return void(f.lastRealTs=n);if(f.logPointer+1>=f.log.length)return f.lastRealTs=n,f.requesting=!0,void(async(e,t,n)=>{const l=await fn.requestReplayData(e,t,n);f.log=f.log.slice(f.logPointer),f.logPointer=0,f.log.push(...l.log),l.log.length<1e4&&(f.final=!0),f.requesting=!1})(e,f.logIdx,1e4);if(f.paused)return void(f.lastRealTs=n);const l=(n-f.lastRealTs)*f.speeds[f.speedIdx],i=f.lastGameTs+l;for(;;){if(f.paused)break;const n=f.logPointer+1;if(n>=f.log.length){f.final&&clearInterval(t);break}const l=f.log[n],o=f.gameStartTs+l[l.length-1];if(o>i)break;const a=l.slice();if(a[0]===Ce){const t=a[1];St.addPlayer(e,t,o),al=!0}else if(a[0]===ke){const t=St.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (update player) ]";St.addPlayer(e,t,o),al=!0}else if(a[0]===Ae){const t=St.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (handle input) ]";const n=a[2];St.handleInput(e,t,n,o),al=!0}f.logPointer=n,f.logIdx++}f.lastRealTs=n,f.lastGameTs=i,M()}),50)}let Y=null;return(e=>{const t=e.fps||60,n=e.slow||1,l=e.update,i=e.render,o=window.requestAnimationFrame,a=1/t,s=n*a;let r,d=0,c=window.performance.now();const u=()=>{for(r=window.performance.now(),d+=Math.min(1,(r-c)/1e3);d>s;)d-=s,l(a);i(d/n),c=r,o(u)};o(u)})({update:()=>{O.createKeyEvents();for(const n of O.consumeAll())if("play"===l){const l=n[0];if(l===Te){const e=n[1],t=n[2];al=!0,E.move(e,t)}else if(l===Ie){if(Y&&!St.getFirstOwnedTile(e,t)){const e={x:n[1],y:n[2]},t=E.worldToViewport(e),l=Math.round(t.x-Y.x),i=Math.round(t.y-Y.y);al=!0,E.move(l,i),Y=t}}else if(l===Be)H(n[1]);else if(l===ze){const e={x:n[1],y:n[2]};Y=E.worldToViewport(e),q(!0)}else if(l===Se)Y=null,q(!1);else if(l===Pe){const e={x:n[1],y:n[2]};al=!0,E.zoom("in",E.worldToViewport(e))}else if(l===_e){const e={x:n[1],y:n[2]};al=!0,E.zoom("out",E.worldToViewport(e))}else l===Oe&&o.togglePreview();const i=w();St.handleInput(e,t,n,i).length>0&&(al=!0),fn.sendClientEvent(n)}else if("replay"===l){const e=n[0];if(e===Te){const e=n[1],t=n[2];al=!0,E.move(e,t)}else if(e===Ie){if(Y){const e={x:n[1],y:n[2]},t=E.worldToViewport(e),l=Math.round(t.x-Y.x),i=Math.round(t.y-Y.y);al=!0,E.move(l,i),Y=t}}else if(e===ze){const e={x:n[1],y:n[2]};Y=E.worldToViewport(e)}else if(e===Se)Y=null;else if(e===Pe){const e={x:n[1],y:n[2]};al=!0,E.zoom("in",E.worldToViewport(e))}else if(e===_e){const e={x:n[1],y:n[2]};al=!0,E.zoom("out",E.worldToViewport(e))}else e===Oe&&o.togglePreview()}$=!!St.getFinishTs(e),R()&&(_.update(),al=!0)},render:async()=>{if(!al)return;const n=w();let i,a,s;window.DEBUG&&Wn(0),B.fillStyle=V(),B.fillRect(0,0,y.width,y.height),window.DEBUG&&qn("clear done"),i=E.worldToViewportRaw(z),a=E.worldDimToViewportRaw(S),B.fillStyle="rgba(255, 255, 255, .3)",B.fillRect(i.x,i.y,a.w,a.h),window.DEBUG&&qn("board done");const r=St.getTilesSortedByZIndex(e);window.DEBUG&&qn("get tiles done"),a=E.worldDimToViewportRaw(I);for(const e of r)(-1===e.owner?il:ol)&&(s=P[e.idx],i=E.worldToViewportRaw({x:b+e.pos.x,y:b+e.pos.y}),B.drawImage(s,0,0,s.width,s.height,i.x,i.y,a.w,a.h));window.DEBUG&&qn("tiles done");const d=[];for(const o of St.getActivePlayers(e,n))c=o,("replay"===l||c.id!==t)&&(s=await m(o),i=E.worldToViewport(o),B.drawImage(s,i.x-u,i.y-p),d.push([`${o.name} (${o.points})`,i.x,i.y+g]));var c;B.fillStyle="white",B.textAlign="center";for(const[e,t,l]of d)B.fillText(e,t,l);window.DEBUG&&qn("players done"),o.setActivePlayers(St.getActivePlayers(e,n)),o.setIdlePlayers(St.getIdlePlayers(e,n)),o.setPiecesDone(St.getFinishedTileCount(e)),window.DEBUG&&qn("HUD done"),R()&&_.render(),al=!1}}),{setHotkeys:e=>{O.setHotkeys(e)},onBgChange:e=>{localStorage.setItem("bg_color",e),O.addEvent([De,e])},onColorChange:e=>{localStorage.setItem("player_color",e),O.addEvent([Be,e])},onNameChange:e=>{localStorage.setItem("player_name",e),O.addEvent([Ee,e])},replayOnSpeedUp:()=>{f.speedIdx+1{f.speedIdx>=1&&(f.speedIdx--,Q())},replayOnPauseToggle:()=>{f.paused=!f.paused,Q()},previewImageUrl:U,player:{background:V(),color:j(),name:St.getPlayerName(e,t)||localStorage.getItem("player_name")||"anon"},disconnect:fn.disconnect,connect:v}}var rl=e({name:"game",components:{PuzzleStatus:Xt,Scores:Qt,SettingsOverlay:tn,PreviewOverlay:an,ConnectionOverlay:wn,HelpOverlay:An},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},disconnect:()=>{},connect:()=>{}}}),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.g=await sl(`${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)}}))},unmounted(){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 dl={id:"game"},cl={class:"menu"},ul={class:"tabs"},gl=s("🧩 Puzzles");rl.render=function(e,i,s,r,d,c){const u=o("settings-overlay"),p=o("preview-overlay"),h=o("help-overlay"),m=o("connection-overlay"),y=o("puzzle-status"),f=o("router-link"),w=o("scores");return a(),t("div",dl,[g(n(u,{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"]),[[v,"settings"===e.overlay]]),g(n(p,{onBgclick:i[3]||(i[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[v,"preview"===e.overlay]]),g(n(h,{onBgclick:i[4]||(i[4]=t=>e.toggle("help",!0))},null,512),[[v,"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",cl,[n("div",ul,[n(f,{class:"opener",to:{name:"index"},target:"_blank"},{default:l((()=>[gl])),_:1}),n("div",{class:"opener",onClick:i[5]||(i[5]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:i[6]||(i[6]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:i[7]||(i[7]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(w,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])};var pl=e({name:"replay",components:{PuzzleStatus:Xt,Scores:Qt,SettingsOverlay:tn,PreviewOverlay:an,HelpOverlay:An},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},replayOnSpeedUp:()=>{},replayOnSpeedDown:()=>{},replayOnPauseToggle:()=>{},disconnect:()=>{}},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.g=await sl(`${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}}))},unmounted(){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 hl={id:"replay"},ml={class:"menu"},yl={class:"tabs"},fl=s("🧩 Puzzles");pl.render=function(e,i,s,d,c,u){const p=o("settings-overlay"),h=o("preview-overlay"),m=o("help-overlay"),y=o("puzzle-status"),f=o("router-link"),w=o("scores");return a(),t("div",hl,[g(n(p,{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"]),[[v,"settings"===e.overlay]]),g(n(h,{onBgclick:i[3]||(i[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[v,"preview"===e.overlay]]),g(n(m,{onBgclick:i[4]||(i[4]=t=>e.toggle("help",!0))},null,512),[[v,"help"===e.overlay]]),n(y,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},{default:l((()=>[n("div",null,[n("div",null,r(e.replayText),1),n("button",{class:"btn",onClick:i[5]||(i[5]=t=>e.g.replayOnSpeedUp())},"⏫"),n("button",{class:"btn",onClick:i[6]||(i[6]=t=>e.g.replayOnSpeedDown())},"⏬"),n("button",{class:"btn",onClick:i[7]||(i[7]=t=>e.g.replayOnPauseToggle())},"⏸️")])])),_:1},8,["finished","duration","piecesDone","piecesTotal"]),n("div",ml,[n("div",yl,[n(f,{class:"opener",to:{name:"index"},target:"_blank"},{default:l((()=>[fl])),_:1}),n("div",{class:"opener",onClick:i[8]||(i[8]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:i[9]||(i[9]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:i[10]||(i[10]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(w,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])},(async()=>{const e=await fetch("/api/conf"),t=await e.json();const n=b({history:x(),routes:[{name:"index",path:"/",component:R},{name:"new-game",path:"/new-game",component:Gt},{name:"game",path:"/g/:id",component:rl},{name:"replay",path:"/replay/:id",component:pl}]});n.beforeEach(((e,t)=>{t.name&&document.documentElement.classList.remove(`view-${String(t.name)}`),document.documentElement.classList.add(`view-${String(e.name)}`)}));const l=C(k);l.config.globalProperties.$config=t,l.config.globalProperties.$clientId=function(){let e=localStorage.getItem("ID");return e||(e=We.uniqId(),localStorage.setItem("ID",e)),e}(),l.use(n),l.mount("#app")})(); diff --git a/build/public/index.html b/build/public/index.html index bf7b9b2..9d4f95b 100644 --- a/build/public/index.html +++ b/build/public/index.html @@ -4,7 +4,7 @@ 🧩 jigsaw.hyottoko.club - + diff --git a/build/server/main.js b/build/server/main.js index 117d365..7438e73 100644 --- a/build/server/main.js +++ b/build/server/main.js @@ -14,271 +14,73 @@ import bodyParser from 'body-parser'; import v8 from 'v8'; import bsqlite from 'better-sqlite3'; -class Rng { - constructor(seed) { - this.rand_high = seed || 0xDEADC0DE; - this.rand_low = seed ^ 0x49616E42; - } - random(min, max) { - this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff; - this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff; - var n = (this.rand_high >>> 0) / 0xffffffff; - return (min + n * (max - min + 1)) | 0; - } - // get one random item from the given array - choice(array) { - return array[this.random(0, array.length - 1)]; - } - // return a shuffled (shallow) copy of the given array - shuffle(array) { - const arr = array.slice(); - for (let i = 0; i <= arr.length - 2; i++) { - const j = this.random(i, arr.length - 1); - const tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - return arr; - } - static serialize(rng) { - return { - rand_high: rng.rand_high, - rand_low: rng.rand_low - }; - } - static unserialize(rngSerialized) { - const rng = new Rng(0); - rng.rand_high = rngSerialized.rand_high; - rng.rand_low = rngSerialized.rand_low; - return rng; - } -} - -const slug = (str) => { - let tmp = str.toLowerCase(); - tmp = tmp.replace(/[^a-z0-9]+/g, '-'); - tmp = tmp.replace(/^-|-$/, ''); - return tmp; -}; -const pad = (x, pad) => { - const str = `${x}`; - if (str.length >= pad.length) { - return str; - } - return pad.substr(0, pad.length - str.length) + str; -}; -const logger = (...pre) => { - const log = (m) => (...args) => { - const d = new Date(); - const hh = pad(d.getHours(), '00'); - const mm = pad(d.getMinutes(), '00'); - const ss = pad(d.getSeconds(), '00'); - console[m](`${hh}:${mm}:${ss}`, ...pre, ...args); - }; +function pointSub(a, b) { + return { x: a.x - b.x, y: a.y - b.y }; +} +function pointAdd(a, b) { + return { x: a.x + b.x, y: a.y + b.y }; +} +function pointDistance(a, b) { + const diffX = a.x - b.x; + const diffY = a.y - b.y; + return Math.sqrt(diffX * diffX + diffY * diffY); +} +function pointInBounds(pt, rect) { + return pt.x >= rect.x + && pt.x <= rect.x + rect.w + && pt.y >= rect.y + && pt.y <= rect.y + rect.h; +} +function rectCenter(rect) { return { - log: log('log'), - error: log('error'), - info: log('info'), + x: rect.x + (rect.w / 2), + y: rect.y + (rect.h / 2), }; -}; -// get a unique id -const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2); -function encodeShape(data) { - /* encoded in 1 byte: - 00000000 - ^^ top - ^^ right - ^^ bottom - ^^ left - */ - return ((data.top + 1) << 0) - | ((data.right + 1) << 2) - | ((data.bottom + 1) << 4) - | ((data.left + 1) << 6); } -function decodeShape(data) { +/** + * Returns a rectangle with same dimensions as the given one, but + * location (x/y) moved by x and y. + * + * @param {x, y, w,, h} rect + * @param number x + * @param number y + * @returns {x, y, w, h} + */ +function rectMoved(rect, x, y) { return { - top: (data >> 0 & 0b11) - 1, - right: (data >> 2 & 0b11) - 1, - bottom: (data >> 4 & 0b11) - 1, - left: (data >> 6 & 0b11) - 1, + x: rect.x + x, + y: rect.y + y, + w: rect.w, + h: rect.h, }; } -function encodeTile(data) { - return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]; +/** + * Returns true if the rectangles overlap, including their borders. + * + * @param {x, y, w, h} rectA + * @param {x, y, w, h} rectB + * @returns bool + */ +function rectsOverlap(rectA, rectB) { + return !(rectB.x > (rectA.x + rectA.w) + || rectA.x > (rectB.x + rectB.w) + || rectB.y > (rectA.y + rectA.h) + || rectA.y > (rectB.y + rectB.h)); } -function decodeTile(data) { - return { - idx: data[0], - pos: { - x: data[1], - y: data[2], - }, - z: data[3], - owner: data[4], - group: data[5], - }; +function rectCenterDistance(rectA, rectB) { + return pointDistance(rectCenter(rectA), rectCenter(rectB)); } -function encodePlayer(data) { - return [ - data.id, - data.x, - data.y, - data.d, - data.name, - data.color, - data.bgcolor, - data.points, - data.ts, - ]; -} -function decodePlayer(data) { - return { - id: data[0], - x: data[1], - y: data[2], - d: data[3], - name: data[4], - color: data[5], - bgcolor: data[6], - points: data[7], - ts: data[8], - }; -} -function encodeGame(data) { - if (Array.isArray(data)) { - return data; - } - return [ - data.id, - data.rng.type, - Rng.serialize(data.rng.obj), - data.puzzle, - data.players, - data.evtInfos, - data.scoreMode, - ]; -} -function decodeGame(data) { - if (!Array.isArray(data)) { - return data; - } - return { - id: data[0], - rng: { - type: data[1], - obj: Rng.unserialize(data[2]), - }, - puzzle: data[3], - players: data[4], - evtInfos: data[5], - scoreMode: data[6], - }; -} -function coordByTileIdx(info, tileIdx) { - const wTiles = info.width / info.tileSize; - return { - x: tileIdx % wTiles, - y: Math.floor(tileIdx / wTiles), - }; -} -const hash = (str) => { - let hash = 0; - for (let i = 0; i < str.length; i++) { - let chr = str.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; - hash |= 0; // Convert to 32bit integer - } - return hash; -}; -function asQueryArgs(data) { - const q = []; - for (let k in data) { - const pair = [k, data[k]].map(encodeURIComponent); - q.push(pair.join('=')); - } - if (q.length === 0) { - return ''; - } - return `?${q.join('&')}`; -} -var Util = { - hash, - slug, - uniqId, - encodeShape, - decodeShape, - encodeTile, - decodeTile, - encodePlayer, - decodePlayer, - encodeGame, - decodeGame, - coordByTileIdx, - asQueryArgs, +var Geometry = { + pointSub, + pointAdd, + pointDistance, + pointInBounds, + rectCenter, + rectMoved, + rectCenterDistance, + rectsOverlap, }; -const log$4 = logger('WebSocketServer.js'); -/* -Example config - -config = { - hostname: 'localhost', - port: 1338, - connectstring: `ws://localhost:1338/ws`, -} -*/ -class EvtBus { - constructor() { - this._on = {}; - } - on(type, callback) { - this._on[type] = this._on[type] || []; - this._on[type].push(callback); - } - dispatch(type, ...args) { - (this._on[type] || []).forEach((cb) => { - cb(...args); - }); - } -} -class WebSocketServer { - constructor(config) { - this.config = config; - this._websocketserver = null; - this.evt = new EvtBus(); - } - on(type, callback) { - this.evt.on(type, callback); - } - listen() { - this._websocketserver = new WebSocket.Server(this.config); - this._websocketserver.on('connection', (socket, request) => { - const pathname = new URL(this.config.connectstring).pathname; - if (request.url.indexOf(pathname) !== 0) { - log$4.log('bad request url: ', request.url); - socket.close(); - return; - } - socket.on('message', (data) => { - log$4.log(`ws`, socket.protocol, data); - this.evt.dispatch('message', { socket, data }); - }); - socket.on('close', () => { - this.evt.dispatch('close', { socket }); - }); - }); - } - close() { - if (this._websocketserver) { - this._websocketserver.close(); - } - } - notifyOne(data, socket) { - socket.send(JSON.stringify(data)); - } -} - /* SERVER_CLIENT_MESSAGE_PROTOCOL NOTE: clients always send game id and their id @@ -364,73 +166,6 @@ var Protocol = { CHANGE_PLAYER, }; -function pointSub(a, b) { - return { x: a.x - b.x, y: a.y - b.y }; -} -function pointAdd(a, b) { - return { x: a.x + b.x, y: a.y + b.y }; -} -function pointDistance(a, b) { - const diffX = a.x - b.x; - const diffY = a.y - b.y; - return Math.sqrt(diffX * diffX + diffY * diffY); -} -function pointInBounds(pt, rect) { - return pt.x >= rect.x - && pt.x <= rect.x + rect.w - && pt.y >= rect.y - && pt.y <= rect.y + rect.h; -} -function rectCenter(rect) { - return { - x: rect.x + (rect.w / 2), - y: rect.y + (rect.h / 2), - }; -} -/** - * Returns a rectangle with same dimensions as the given one, but - * location (x/y) moved by x and y. - * - * @param {x, y, w,, h} rect - * @param number x - * @param number y - * @returns {x, y, w, h} - */ -function rectMoved(rect, x, y) { - return { - x: rect.x + x, - y: rect.y + y, - w: rect.w, - h: rect.h, - }; -} -/** - * Returns true if the rectangles overlap, including their borders. - * - * @param {x, y, w, h} rectA - * @param {x, y, w, h} rectB - * @returns bool - */ -function rectsOverlap(rectA, rectB) { - return !(rectB.x > (rectA.x + rectA.w) - || rectA.x > (rectB.x + rectB.w) - || rectB.y > (rectA.y + rectA.h) - || rectA.y > (rectB.y + rectB.h)); -} -function rectCenterDistance(rectA, rectB) { - return pointDistance(rectCenter(rectA), rectCenter(rectB)); -} -var Geometry = { - pointSub, - pointAdd, - pointDistance, - pointInBounds, - rectCenter, - rectMoved, - rectCenterDistance, - rectsOverlap, -}; - const MS = 1; const SEC = MS * 1000; const MIN = SEC * 60; @@ -527,8 +262,8 @@ function setPlayer(gameId, playerId, player) { GAMES[gameId].players[idx] = Util.encodePlayer(player); } } -function setTile(gameId, tileIdx, tile) { - GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile); +function setPiece(gameId, pieceIdx, piece) { + GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece); } function setPuzzleData(gameId, data) { GAMES[gameId].puzzle.data = data; @@ -583,7 +318,7 @@ function getAllPlayers(gameId) { function get$1(gameId) { return GAMES[gameId]; } -function getTileCount(gameId) { +function getPieceCount(gameId) { return GAMES[gameId].puzzle.tiles.length; } function getImageUrl(gameId) { @@ -596,20 +331,20 @@ function getScoreMode(gameId) { return GAMES[gameId].scoreMode || ScoreMode.FINAL; } function isFinished(gameId) { - return getFinishedTileCount(gameId) === getTileCount(gameId); + return getFinishedPiecesCount(gameId) === getPieceCount(gameId); } -function getFinishedTileCount(gameId) { +function getFinishedPiecesCount(gameId) { let count = 0; for (let t of GAMES[gameId].puzzle.tiles) { - if (Util.decodeTile(t).owner === -1) { + if (Util.decodePiece(t).owner === -1) { count++; } } return count; } -function getTilesSortedByZIndex(gameId) { - const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile); - return tiles.sort((t1, t2) => t1.z - t2.z); +function getPiecesSortedByZIndex(gameId) { + const pieces = GAMES[gameId].puzzle.tiles.map(Util.decodePiece); + return pieces.sort((t1, t2) => t1.z - t2.z); } function changePlayer(gameId, playerId, change) { const player = getPlayer(gameId, playerId); @@ -628,22 +363,22 @@ function changeData(gameId, change) { GAMES[gameId].puzzle.data[k] = change[k]; } } -function changeTile(gameId, tileIdx, change) { +function changeTile(gameId, pieceIdx, change) { for (let k of Object.keys(change)) { - const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]); + const piece = Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx]); // @ts-ignore - tile[k] = change[k]; - GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile); + piece[k] = change[k]; + GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece); } } -const getTile = (gameId, tileIdx) => { - return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]); +const getPiece = (gameId, pieceIdx) => { + return Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx]); }; -const getTileGroup = (gameId, tileIdx) => { - const tile = getTile(gameId, tileIdx); +const getPieceGroup = (gameId, tileIdx) => { + const tile = getPiece(gameId, tileIdx); return tile.group; }; -const getFinalTilePos = (gameId, tileIdx) => { +const getFinalPiecePos = (gameId, tileIdx) => { const info = GAMES[gameId].puzzle.info; const boardPos = { x: (info.table.width - info.width) / 2, @@ -652,8 +387,8 @@ const getFinalTilePos = (gameId, tileIdx) => { const srcPos = srcPosByTileIdx(gameId, tileIdx); return Geometry.pointAdd(boardPos, srcPos); }; -const getTilePos = (gameId, tileIdx) => { - const tile = getTile(gameId, tileIdx); +const getPiecePos = (gameId, tileIdx) => { + const tile = getPiece(gameId, tileIdx); return tile.pos; }; // todo: instead, just make the table bigger and use that :) @@ -669,9 +404,9 @@ const getBounds = (gameId) => { h: th + 2 * overY, }; }; -const getTileBounds = (gameId, tileIdx) => { - const s = getTileSize(gameId); - const tile = getTile(gameId, tileIdx); +const getPieceBounds = (gameId, tileIdx) => { + const s = getPieceSize(gameId); + const tile = getPiece(gameId, tileIdx); return { x: tile.pos.x, y: tile.pos.y, @@ -680,29 +415,29 @@ const getTileBounds = (gameId, tileIdx) => { }; }; const getTileZIndex = (gameId, tileIdx) => { - const tile = getTile(gameId, tileIdx); + const tile = getPiece(gameId, tileIdx); return tile.z; }; -const getFirstOwnedTileIdx = (gameId, playerId) => { +const getFirstOwnedPieceIdx = (gameId, playerId) => { for (let t of GAMES[gameId].puzzle.tiles) { - const tile = Util.decodeTile(t); + const tile = Util.decodePiece(t); if (tile.owner === playerId) { return tile.idx; } } return -1; }; -const getFirstOwnedTile = (gameId, playerId) => { - const idx = getFirstOwnedTileIdx(gameId, playerId); +const getFirstOwnedPiece = (gameId, playerId) => { + const idx = getFirstOwnedPieceIdx(gameId, playerId); return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]; }; -const getTileDrawOffset = (gameId) => { +const getPieceDrawOffset = (gameId) => { return GAMES[gameId].puzzle.info.tileDrawOffset; }; -const getTileDrawSize = (gameId) => { +const getPieceDrawSize = (gameId) => { return GAMES[gameId].puzzle.info.tileDrawSize; }; -const getTileSize = (gameId) => { +const getPieceSize = (gameId) => { return GAMES[gameId].puzzle.info.tileSize; }; const getStartTs = (gameId) => { @@ -754,27 +489,27 @@ const setTilesZIndex = (gameId, tileIdxs, zIndex) => { } }; const moveTileDiff = (gameId, tileIdx, diff) => { - const oldPos = getTilePos(gameId, tileIdx); + const oldPos = getPiecePos(gameId, tileIdx); const pos = Geometry.pointAdd(oldPos, diff); changeTile(gameId, tileIdx, { pos }); }; const moveTilesDiff = (gameId, tileIdxs, diff) => { - const tileDrawSize = getTileDrawSize(gameId); + const drawSize = getPieceDrawSize(gameId); const bounds = getBounds(gameId); const cappedDiff = diff; for (let tileIdx of tileIdxs) { - const t = getTile(gameId, tileIdx); + const t = getPiece(gameId, tileIdx); if (t.pos.x + diff.x < bounds.x) { cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x); } - else if (t.pos.x + tileDrawSize + diff.x > bounds.x + bounds.w) { - cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + tileDrawSize, cappedDiff.x); + else if (t.pos.x + drawSize + diff.x > bounds.x + bounds.w) { + cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + drawSize, cappedDiff.x); } if (t.pos.y + diff.y < bounds.y) { cappedDiff.y = Math.max(bounds.y - t.pos.y, cappedDiff.y); } - else if (t.pos.y + tileDrawSize + diff.y > bounds.y + bounds.h) { - cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + tileDrawSize, cappedDiff.y); + else if (t.pos.y + drawSize + diff.y > bounds.y + bounds.h) { + cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + drawSize, cappedDiff.y); } } for (let tileIdx of tileIdxs) { @@ -792,49 +527,49 @@ const setTilesOwner = (gameId, tileIdxs, owner) => { } }; // get all grouped tiles for a tile -function getGroupedTileIdxs(gameId, tileIdx) { - const tiles = GAMES[gameId].puzzle.tiles; - const tile = Util.decodeTile(tiles[tileIdx]); +function getGroupedPieceIdxs(gameId, pieceIdx) { + const pieces = GAMES[gameId].puzzle.tiles; + const piece = Util.decodePiece(pieces[pieceIdx]); const grouped = []; - if (tile.group) { - for (let other of tiles) { - const otherTile = Util.decodeTile(other); - if (otherTile.group === tile.group) { - grouped.push(otherTile.idx); + if (piece.group) { + for (let other of pieces) { + const otherPiece = Util.decodePiece(other); + if (otherPiece.group === piece.group) { + grouped.push(otherPiece.idx); } } } else { - grouped.push(tile.idx); + grouped.push(piece.idx); } return grouped; } // Returns the index of the puzzle tile with the highest z index // that is not finished yet and that matches the position -const freeTileIdxByPos = (gameId, pos) => { +const freePieceIdxByPos = (gameId, pos) => { let info = GAMES[gameId].puzzle.info; - let tiles = GAMES[gameId].puzzle.tiles; + let pieces = GAMES[gameId].puzzle.tiles; let maxZ = -1; - let tileIdx = -1; - for (let idx = 0; idx < tiles.length; idx++) { - const tile = Util.decodeTile(tiles[idx]); - if (tile.owner !== 0) { + let pieceIdx = -1; + for (let idx = 0; idx < pieces.length; idx++) { + const piece = Util.decodePiece(pieces[idx]); + if (piece.owner !== 0) { continue; } const collisionRect = { - x: tile.pos.x, - y: tile.pos.y, + x: piece.pos.x, + y: piece.pos.y, w: info.tileSize, h: info.tileSize, }; if (Geometry.pointInBounds(pos, collisionRect)) { - if (maxZ === -1 || tile.z > maxZ) { - maxZ = tile.z; - tileIdx = idx; + if (maxZ === -1 || piece.z > maxZ) { + maxZ = piece.z; + pieceIdx = idx; } } } - return tileIdx; + return pieceIdx; }; const getPlayerBgColor = (gameId, playerId) => { const p = getPlayer(gameId, playerId); @@ -854,8 +589,8 @@ const getPlayerPoints = (gameId, playerId) => { }; // determine if two tiles are grouped together const areGrouped = (gameId, tileIdx1, tileIdx2) => { - const g1 = getTileGroup(gameId, tileIdx1); - const g2 = getTileGroup(gameId, tileIdx2); + const g1 = getPieceGroup(gameId, tileIdx1); + const g2 = getPieceGroup(gameId, tileIdx2); return !!(g1 && g1 === g2); }; const getTableWidth = (gameId) => { @@ -886,7 +621,7 @@ function handleInput$1(gameId, playerId, input, ts) { const _tileChange = (tileIdx) => { changes.push([ Protocol.CHANGE_TILE, - Util.encodeTile(getTile(gameId, tileIdx)), + Util.encodePiece(getPiece(gameId, tileIdx)), ]); }; const _tileChanges = (tileIdxs) => { @@ -907,8 +642,8 @@ function handleInput$1(gameId, playerId, input, ts) { // put both tiles (and their grouped tiles) in the same group const groupTiles = (gameId, tileIdx1, tileIdx2) => { const tiles = GAMES[gameId].puzzle.tiles; - const group1 = getTileGroup(gameId, tileIdx1); - const group2 = getTileGroup(gameId, tileIdx2); + const group1 = getPieceGroup(gameId, tileIdx1); + const group2 = getPieceGroup(gameId, tileIdx2); let group; const searchGroups = []; if (group1) { @@ -936,10 +671,10 @@ function handleInput$1(gameId, playerId, input, ts) { // TODO: strange if (searchGroups.length > 0) { for (const t of tiles) { - const tile = Util.decodeTile(t); - if (searchGroups.includes(tile.group)) { - changeTile(gameId, tile.idx, { group }); - _tileChange(tile.idx); + const piece = Util.decodePiece(t); + if (searchGroups.includes(piece.group)) { + changeTile(gameId, piece.idx, { group }); + _tileChange(piece.idx); } } } @@ -967,12 +702,12 @@ function handleInput$1(gameId, playerId, input, ts) { changePlayer(gameId, playerId, { d: 1, ts }); _playerChange(); evtInfo._last_mouse_down = pos; - const tileIdxAtPos = freeTileIdxByPos(gameId, pos); + const tileIdxAtPos = freePieceIdxByPos(gameId, pos); if (tileIdxAtPos >= 0) { let maxZ = getMaxZIndex(gameId) + 1; changeData(gameId, { maxZ }); _dataChange(); - const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos); + const tileIdxs = getGroupedPieceIdxs(gameId, tileIdxAtPos); setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId)); setTilesOwner(gameId, tileIdxs, playerId); _tileChanges(tileIdxs); @@ -989,18 +724,18 @@ function handleInput$1(gameId, playerId, input, ts) { _playerChange(); } else { - let tileIdx = getFirstOwnedTileIdx(gameId, playerId); + let tileIdx = getFirstOwnedPieceIdx(gameId, playerId); if (tileIdx >= 0) { // player is moving a tile (and hand) changePlayer(gameId, playerId, { x, y, ts }); _playerChange(); // check if pos is on the tile, otherwise dont move // (mouse could be out of table, but tile stays on it) - const tileIdxs = getGroupedTileIdxs(gameId, tileIdx); + const tileIdxs = getGroupedPieceIdxs(gameId, tileIdx); let anyOk = Geometry.pointInBounds(pos, getBounds(gameId)) && Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId)); for (let idx of tileIdxs) { - const bounds = getTileBounds(gameId, idx); + const bounds = getPieceBounds(gameId, idx); if (Geometry.pointInBounds(pos, bounds)) { anyOk = true; break; @@ -1029,15 +764,15 @@ function handleInput$1(gameId, playerId, input, ts) { const pos = { x, y }; const d = 0; evtInfo._last_mouse_down = null; - let tileIdx = getFirstOwnedTileIdx(gameId, playerId); + let tileIdx = getFirstOwnedPieceIdx(gameId, playerId); if (tileIdx >= 0) { // drop the tile(s) - let tileIdxs = getGroupedTileIdxs(gameId, tileIdx); + let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx); setTilesOwner(gameId, tileIdxs, 0); _tileChanges(tileIdxs); // Check if the tile was dropped near the final location - let tilePos = getTilePos(gameId, tileIdx); - let finalPos = getFinalTilePos(gameId, tileIdx); + let tilePos = getPiecePos(gameId, tileIdx); + let finalPos = getFinalPiecePos(gameId, tileIdx); if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) { let diff = Geometry.pointSub(finalPos, tilePos); // Snap the tile to the final destination @@ -1055,7 +790,7 @@ function handleInput$1(gameId, playerId, input, ts) { changePlayer(gameId, playerId, { d, ts, points }); _playerChange(); // check if the puzzle is finished - if (getFinishedTileCount(gameId) === getTileCount(gameId)) { + if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) { changeData(gameId, { finished: ts }); _dataChange(); } @@ -1070,14 +805,14 @@ function handleInput$1(gameId, playerId, input, ts) { if (areGrouped(gameId, tileIdx, otherTileIdx)) { return false; } - const tilePos = getTilePos(gameId, tileIdx); - const dstPos = Geometry.pointAdd(getTilePos(gameId, otherTileIdx), { x: off[0] * info.tileSize, y: off[1] * info.tileSize }); + const tilePos = getPiecePos(gameId, tileIdx); + const dstPos = Geometry.pointAdd(getPiecePos(gameId, otherTileIdx), { x: off[0] * info.tileSize, y: off[1] * info.tileSize }); if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) { let diff = Geometry.pointSub(dstPos, tilePos); - let tileIdxs = getGroupedTileIdxs(gameId, tileIdx); + let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx); moveTilesDiff(gameId, tileIdxs, diff); groupTiles(gameId, tileIdx, otherTileIdx); - tileIdxs = getGroupedTileIdxs(gameId, tileIdx); + tileIdxs = getGroupedPieceIdxs(gameId, tileIdx); const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs); setTilesZIndex(gameId, tileIdxs, zIndex); _tileChanges(tileIdxs); @@ -1086,7 +821,7 @@ function handleInput$1(gameId, playerId, input, ts) { return false; }; let snapped = false; - for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) { + for (let tileIdxTmp of getGroupedPieceIdxs(gameId, tileIdx)) { let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp); if (check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top || check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right @@ -1136,15 +871,14 @@ function handleInput$1(gameId, playerId, input, ts) { return changes; } var GameCommon = { - __createPlayerObject, setGame, exists: exists$1, playerExists, getActivePlayers, getIdlePlayers, addPlayer: addPlayer$1, - getFinishedTileCount, - getTileCount, + getFinishedPiecesCount, + getPieceCount, getImageUrl, setImageUrl, get: get$1, @@ -1156,7 +890,7 @@ var GameCommon = { getPlayerIdByIndex, changePlayer, setPlayer, - setTile, + setPiece, setPuzzleData, getTableWidth, getTableHeight, @@ -1164,16 +898,275 @@ var GameCommon = { getRng, getPuzzleWidth, getPuzzleHeight, - getTilesSortedByZIndex, - getFirstOwnedTile, - getTileDrawOffset, - getTileDrawSize, - getFinalTilePos, + getPiecesSortedByZIndex, + getFirstOwnedPiece, + getPieceDrawOffset, + getPieceDrawSize, + getFinalPiecePos, getStartTs, getFinishTs, handleInput: handleInput$1, }; +class Rng { + constructor(seed) { + this.rand_high = seed || 0xDEADC0DE; + this.rand_low = seed ^ 0x49616E42; + } + random(min, max) { + this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff; + this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff; + var n = (this.rand_high >>> 0) / 0xffffffff; + return (min + n * (max - min + 1)) | 0; + } + // get one random item from the given array + choice(array) { + return array[this.random(0, array.length - 1)]; + } + // return a shuffled (shallow) copy of the given array + shuffle(array) { + const arr = array.slice(); + for (let i = 0; i <= arr.length - 2; i++) { + const j = this.random(i, arr.length - 1); + const tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + return arr; + } + static serialize(rng) { + return { + rand_high: rng.rand_high, + rand_low: rng.rand_low + }; + } + static unserialize(rngSerialized) { + const rng = new Rng(0); + rng.rand_high = rngSerialized.rand_high; + rng.rand_low = rngSerialized.rand_low; + return rng; + } +} + +const slug = (str) => { + let tmp = str.toLowerCase(); + tmp = tmp.replace(/[^a-z0-9]+/g, '-'); + tmp = tmp.replace(/^-|-$/, ''); + return tmp; +}; +const pad = (x, pad) => { + const str = `${x}`; + if (str.length >= pad.length) { + return str; + } + return pad.substr(0, pad.length - str.length) + str; +}; +const logger = (...pre) => { + const log = (m) => (...args) => { + const d = new Date(); + const hh = pad(d.getHours(), '00'); + const mm = pad(d.getMinutes(), '00'); + const ss = pad(d.getSeconds(), '00'); + console[m](`${hh}:${mm}:${ss}`, ...pre, ...args); + }; + return { + log: log('log'), + error: log('error'), + info: log('info'), + }; +}; +// get a unique id +const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2); +function encodeShape(data) { + /* encoded in 1 byte: + 00000000 + ^^ top + ^^ right + ^^ bottom + ^^ left + */ + return ((data.top + 1) << 0) + | ((data.right + 1) << 2) + | ((data.bottom + 1) << 4) + | ((data.left + 1) << 6); +} +function decodeShape(data) { + return { + top: (data >> 0 & 0b11) - 1, + right: (data >> 2 & 0b11) - 1, + bottom: (data >> 4 & 0b11) - 1, + left: (data >> 6 & 0b11) - 1, + }; +} +function encodePiece(data) { + return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]; +} +function decodePiece(data) { + return { + idx: data[0], + pos: { + x: data[1], + y: data[2], + }, + z: data[3], + owner: data[4], + group: data[5], + }; +} +function encodePlayer(data) { + return [ + data.id, + data.x, + data.y, + data.d, + data.name, + data.color, + data.bgcolor, + data.points, + data.ts, + ]; +} +function decodePlayer(data) { + return { + id: data[0], + x: data[1], + y: data[2], + d: data[3], + name: data[4], + color: data[5], + bgcolor: data[6], + points: data[7], + ts: data[8], + }; +} +function encodeGame(data) { + return [ + data.id, + data.rng.type || '', + Rng.serialize(data.rng.obj), + data.puzzle, + data.players, + data.evtInfos, + data.scoreMode || ScoreMode.FINAL, + ]; +} +function decodeGame(data) { + return { + id: data[0], + rng: { + type: data[1], + obj: Rng.unserialize(data[2]), + }, + puzzle: data[3], + players: data[4], + evtInfos: data[5], + scoreMode: data[6], + }; +} +function coordByTileIdx(info, tileIdx) { + const wTiles = info.width / info.tileSize; + return { + x: tileIdx % wTiles, + y: Math.floor(tileIdx / wTiles), + }; +} +const hash = (str) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + let chr = str.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; +}; +function asQueryArgs(data) { + const q = []; + for (let k in data) { + const pair = [k, data[k]].map(encodeURIComponent); + q.push(pair.join('=')); + } + if (q.length === 0) { + return ''; + } + return `?${q.join('&')}`; +} +var Util = { + hash, + slug, + uniqId, + encodeShape, + decodeShape, + encodePiece, + decodePiece, + encodePlayer, + decodePlayer, + encodeGame, + decodeGame, + coordByTileIdx, + asQueryArgs, +}; + +const log$4 = logger('WebSocketServer.js'); +/* +Example config + +config = { + hostname: 'localhost', + port: 1338, + connectstring: `ws://localhost:1338/ws`, +} +*/ +class EvtBus { + constructor() { + this._on = {}; + } + on(type, callback) { + this._on[type] = this._on[type] || []; + this._on[type].push(callback); + } + dispatch(type, ...args) { + (this._on[type] || []).forEach((cb) => { + cb(...args); + }); + } +} +class WebSocketServer { + constructor(config) { + this.config = config; + this._websocketserver = null; + this.evt = new EvtBus(); + } + on(type, callback) { + this.evt.on(type, callback); + } + listen() { + this._websocketserver = new WebSocket.Server(this.config); + this._websocketserver.on('connection', (socket, request) => { + const pathname = new URL(this.config.connectstring).pathname; + if (request.url.indexOf(pathname) !== 0) { + log$4.log('bad request url: ', request.url); + socket.close(); + return; + } + socket.on('message', (data) => { + log$4.log(`ws`, socket.protocol, data); + this.evt.dispatch('message', { socket, data }); + }); + socket.on('close', () => { + this.evt.dispatch('close', { socket }); + }); + }); + } + close() { + if (this._websocketserver) { + this._websocketserver.close(); + } + } + notifyOne(data, socket) { + socket.send(JSON.stringify(data)); + } +} + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const BASE_DIR = `${__dirname}/../..`; @@ -1409,7 +1402,7 @@ async function createPuzzle(rng, targetTiles, image, ts) { if (!dim.w || !dim.h) { throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`; } - const info = determinePuzzleInfo(dim.w, dim.h, targetTiles); + const info = determinePuzzleInfo(dim, targetTiles); let tiles = new Array(info.tiles); for (let i = 0; i < tiles.length; i++) { tiles[i] = { idx: i }; @@ -1464,7 +1457,7 @@ async function createPuzzle(rng, targetTiles, image, ts) { // then shuffle the positions positions = rng.shuffle(positions); const pieces = tiles.map(tile => { - return Util.encodeTile({ + return Util.encodePiece({ idx: tile.idx, group: 0, z: 0, @@ -1537,9 +1530,9 @@ function determinePuzzleTileShapes(rng, info) { } return shapes.map(Util.encodeShape); } -const determineTilesXY = (w, h, targetTiles) => { - const w_ = w < h ? (w * h) : (w * w); - const h_ = w < h ? (h * h) : (w * h); +const determineTilesXY = (dim, targetTiles) => { + const w_ = dim.w < dim.h ? (dim.w * dim.h) : (dim.w * dim.w); + const h_ = dim.w < dim.h ? (dim.h * dim.h) : (dim.w * dim.h); let size = 0; let tiles = 0; do { @@ -1552,8 +1545,8 @@ const determineTilesXY = (w, h, targetTiles) => { tilesY: Math.round(h_ / size), }; }; -const determinePuzzleInfo = (w, h, targetTiles) => { - const { tilesX, tilesY } = determineTilesXY(w, h, targetTiles); +const determinePuzzleInfo = (dim, targetTiles) => { + const { tilesX, tilesY } = determineTilesXY(dim, targetTiles); const tiles = tilesX * tilesY; const tileSize = TILE_SIZE; const width = tilesX * tileSize; @@ -1606,7 +1599,7 @@ function loadGame(gameId) { } if (typeof game.puzzle.data.finished === 'undefined') { const unfinished = game.puzzle.tiles - .map(Util.decodeTile) + .map(Util.decodePiece) .find((t) => t.owner !== -1); game.puzzle.data.finished = unfinished ? 0 : Time.timestamp(); } @@ -1700,16 +1693,6 @@ var Game = { createGame, addPlayer, handleInput, - getAllGames: GameCommon.getAllGames, - getActivePlayers: GameCommon.getActivePlayers, - getFinishedTileCount: GameCommon.getFinishedTileCount, - getImageUrl: GameCommon.getImageUrl, - getTileCount: GameCommon.getTileCount, - exists: GameCommon.exists, - playerExists: GameCommon.playerExists, - get: GameCommon.get, - getStartTs: GameCommon.getStartTs, - getFinishTs: GameCommon.getFinishTs, }; const log$2 = logger('GameSocket.js'); @@ -1966,15 +1949,15 @@ app.get('/api/newgame-data', (req, res) => { app.get('/api/index-data', (req, res) => { const ts = Time.timestamp(); const games = [ - ...Game.getAllGames().map((game) => ({ + ...GameCommon.getAllGames().map((game) => ({ id: game.id, hasReplay: GameLog.exists(game.id), - started: Game.getStartTs(game.id), - finished: Game.getFinishTs(game.id), - tilesFinished: Game.getFinishedTileCount(game.id), - tilesTotal: Game.getTileCount(game.id), - players: Game.getActivePlayers(game.id, ts).length, - imageUrl: Game.getImageUrl(game.id), + started: GameCommon.getStartTs(game.id), + finished: GameCommon.getFinishTs(game.id), + tilesFinished: GameCommon.getFinishedPiecesCount(game.id), + tilesTotal: GameCommon.getPieceCount(game.id), + players: GameCommon.getActivePlayers(game.id, ts).length, + imageUrl: GameCommon.getImageUrl(game.id), })), ]; res.send({ @@ -2036,7 +2019,7 @@ app.post('/newgame', bodyParser.json(), async (req, res) => { const gameSettings = req.body; log.log(gameSettings); const gameId = Util.uniqId(); - if (!Game.exists(gameId)) { + if (!GameCommon.exists(gameId)) { const ts = Time.timestamp(); await Game.createGame(gameId, gameSettings.tiles, gameSettings.image, ts, gameSettings.scoreMode); } @@ -2072,26 +2055,26 @@ wss.on('message', async ({ socket, data }) => { switch (msgType) { case Protocol.EV_CLIENT_INIT: { - if (!Game.exists(gameId)) { + if (!GameCommon.exists(gameId)) { throw `[game ${gameId} does not exist... ]`; } const ts = Time.timestamp(); Game.addPlayer(gameId, clientId, ts); GameSockets.addSocket(gameId, socket); - const game = Game.get(gameId); + const game = GameCommon.get(gameId); notify([Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [socket]); } break; case Protocol.EV_CLIENT_EVENT: { - if (!Game.exists(gameId)) { + if (!GameCommon.exists(gameId)) { throw `[game ${gameId} does not exist... ]`; } const clientSeq = msg[1]; const clientEvtData = msg[2]; const ts = Time.timestamp(); let sendGame = false; - if (!Game.playerExists(gameId, clientId)) { + if (!GameCommon.playerExists(gameId, clientId)) { Game.addPlayer(gameId, clientId, ts); sendGame = true; } @@ -2100,7 +2083,7 @@ wss.on('message', async ({ socket, data }) => { sendGame = true; } if (sendGame) { - const game = Game.get(gameId); + const game = GameCommon.get(gameId); notify([Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [socket]); } const changes = Game.handleInput(gameId, clientId, clientEvtData, ts); diff --git a/scripts/fix_tiles.ts b/scripts/fix_tiles.ts index 6da6aef..5eb7ee9 100644 --- a/scripts/fix_tiles.ts +++ b/scripts/fix_tiles.ts @@ -7,22 +7,22 @@ const log = logger('fix_tiles.js') function fix_tiles(gameId) { GameStorage.loadGame(gameId) let changed = false - const tiles = GameCommon.getTilesSortedByZIndex(gameId) + const tiles = GameCommon.getPiecesSortedByZIndex(gameId) for (let tile of tiles) { if (tile.owner === -1) { - const p = GameCommon.getFinalTilePos(gameId, tile.idx) + const p = GameCommon.getFinalPiecePos(gameId, tile.idx) if (p.x === tile.pos.x && p.y === tile.pos.y) { // log.log('all good', tile.pos) } else { log.log('bad tile pos', tile.pos, 'should be: ', p) tile.pos = p - GameCommon.setTile(gameId, tile.idx, tile) + GameCommon.setPiece(gameId, tile.idx, tile) changed = true } } else if (tile.owner !== 0) { tile.owner = 0 log.log('unowning tile', tile.idx) - GameCommon.setTile(gameId, tile.idx, tile) + GameCommon.setPiece(gameId, tile.idx, tile) changed = true } } diff --git a/src/common/GameCommon.ts b/src/common/GameCommon.ts index f498009..e9e2d30 100644 --- a/src/common/GameCommon.ts +++ b/src/common/GameCommon.ts @@ -1,14 +1,50 @@ import Geometry, { Point, Rect } from './Geometry' import Protocol from './Protocol' -import { Rng } from './Rng' +import { Rng, RngSerialized } from './Rng' import Time from './Time' +import { FixedLengthArray } from './Types' import Util from './Util' export type Timestamp = number -export type EncodedPlayer = Array -export type EncodedPiece = Array + +export type EncodedPlayer = FixedLengthArray<[ + string, + number, + number, + 0|1, + string|null, + string|null, + string|null, + number, + Timestamp, +]> + +export type EncodedPiece = FixedLengthArray<[ + number, + number, + number, + number, + string|number, + number, +]> + export type EncodedPieceShape = number +export type EncodedGame = FixedLengthArray<[ + string, + string, + RngSerialized, + Puzzle, + Array, + Record, + ScoreMode, +]> + +export interface ReplayData { + log: any[], + game: EncodedGame|null +} + export interface Tag { id: number slug: string @@ -20,7 +56,7 @@ interface GameRng { type?: string } -interface Game { +export interface Game { id: string players: Array puzzle: Puzzle @@ -112,7 +148,7 @@ export interface Player { color: string|null bgcolor: string|null points: number - ts: number + ts: Timestamp } interface EvtInfo { @@ -134,7 +170,7 @@ function exists(gameId: string) { return (!!GAMES[gameId]) || false } -function __createPlayerObject(id: string, ts: number): Player { +function __createPlayerObject(id: string, ts: Timestamp): Player { return { id: id, x: 0, @@ -191,8 +227,8 @@ function setPlayer( } } -function setTile(gameId: string, tileIdx: number, tile: Piece): void { - GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile) +function setPiece(gameId: string, pieceIdx: number, piece: Piece): void { + GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece) } function setPuzzleData(gameId: string, data: PuzzleData): void { @@ -214,7 +250,7 @@ function getIdlePlayers(gameId: string, ts: number): Array { return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0) } -function addPlayer(gameId: string, playerId: string, ts: number): void { +function addPlayer(gameId: string, playerId: string, ts: Timestamp): void { if (!playerExists(gameId, playerId)) { setPlayer(gameId, playerId, __createPlayerObject(playerId, ts)) } else { @@ -261,7 +297,7 @@ function get(gameId: string) { return GAMES[gameId] } -function getTileCount(gameId: string): number { +function getPieceCount(gameId: string): number { return GAMES[gameId].puzzle.tiles.length } @@ -278,22 +314,22 @@ function getScoreMode(gameId: string): ScoreMode { } function isFinished(gameId: string): boolean { - return getFinishedTileCount(gameId) === getTileCount(gameId) + return getFinishedPiecesCount(gameId) === getPieceCount(gameId) } -function getFinishedTileCount(gameId: string): number { +function getFinishedPiecesCount(gameId: string): number { let count = 0 for (let t of GAMES[gameId].puzzle.tiles) { - if (Util.decodeTile(t).owner === -1) { + if (Util.decodePiece(t).owner === -1) { count++ } } return count } -function getTilesSortedByZIndex(gameId: string): Piece[] { - const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile) - return tiles.sort((t1, t2) => t1.z - t2.z) +function getPiecesSortedByZIndex(gameId: string): Piece[] { + const pieces = GAMES[gameId].puzzle.tiles.map(Util.decodePiece) + return pieces.sort((t1, t2) => t1.z - t2.z) } function changePlayer( @@ -320,25 +356,25 @@ function changeData(gameId: string, change: any): void { } } -function changeTile(gameId: string, tileIdx: number, change: any): void { +function changeTile(gameId: string, pieceIdx: number, change: any): void { for (let k of Object.keys(change)) { - const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]) + const piece = Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx]) // @ts-ignore - tile[k] = change[k] - GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile) + piece[k] = change[k] + GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece) } } -const getTile = (gameId: string, tileIdx: number): Piece => { - return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]) +const getPiece = (gameId: string, pieceIdx: number): Piece => { + return Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx]) } -const getTileGroup = (gameId: string, tileIdx: number): number => { - const tile = getTile(gameId, tileIdx) +const getPieceGroup = (gameId: string, tileIdx: number): number => { + const tile = getPiece(gameId, tileIdx) return tile.group } -const getFinalTilePos = (gameId: string, tileIdx: number): Point => { +const getFinalPiecePos = (gameId: string, tileIdx: number): Point => { const info = GAMES[gameId].puzzle.info const boardPos = { x: (info.table.width - info.width) / 2, @@ -348,8 +384,8 @@ const getFinalTilePos = (gameId: string, tileIdx: number): Point => { return Geometry.pointAdd(boardPos, srcPos) } -const getTilePos = (gameId: string, tileIdx: number): Point => { - const tile = getTile(gameId, tileIdx) +const getPiecePos = (gameId: string, tileIdx: number): Point => { + const tile = getPiece(gameId, tileIdx) return tile.pos } @@ -368,9 +404,9 @@ const getBounds = (gameId: string): Rect => { } } -const getTileBounds = (gameId: string, tileIdx: number): Rect => { - const s = getTileSize(gameId) - const tile = getTile(gameId, tileIdx) +const getPieceBounds = (gameId: string, tileIdx: number): Rect => { + const s = getPieceSize(gameId) + const tile = getPiece(gameId, tileIdx) return { x: tile.pos.x, y: tile.pos.y, @@ -380,13 +416,13 @@ const getTileBounds = (gameId: string, tileIdx: number): Rect => { } const getTileZIndex = (gameId: string, tileIdx: number): number => { - const tile = getTile(gameId, tileIdx) + const tile = getPiece(gameId, tileIdx) return tile.z } -const getFirstOwnedTileIdx = (gameId: string, playerId: string): number => { +const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => { for (let t of GAMES[gameId].puzzle.tiles) { - const tile = Util.decodeTile(t) + const tile = Util.decodePiece(t) if (tile.owner === playerId) { return tile.idx } @@ -394,20 +430,20 @@ const getFirstOwnedTileIdx = (gameId: string, playerId: string): number => { return -1 } -const getFirstOwnedTile = (gameId: string, playerId: string): EncodedPiece|null => { - const idx = getFirstOwnedTileIdx(gameId, playerId) +const getFirstOwnedPiece = (gameId: string, playerId: string): EncodedPiece|null => { + const idx = getFirstOwnedPieceIdx(gameId, playerId) return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx] } -const getTileDrawOffset = (gameId: string): number => { +const getPieceDrawOffset = (gameId: string): number => { return GAMES[gameId].puzzle.info.tileDrawOffset } -const getTileDrawSize = (gameId: string): number => { +const getPieceDrawSize = (gameId: string): number => { return GAMES[gameId].puzzle.info.tileDrawSize } -const getTileSize = (gameId: string): number => { +const getPieceSize = (gameId: string): number => { return GAMES[gameId].puzzle.info.tileSize } @@ -472,7 +508,7 @@ const setTilesZIndex = (gameId: string, tileIdxs: Array, zIndex: number) } const moveTileDiff = (gameId: string, tileIdx: number, diff: Point): void => { - const oldPos = getTilePos(gameId, tileIdx) + const oldPos = getPiecePos(gameId, tileIdx) const pos = Geometry.pointAdd(oldPos, diff) changeTile(gameId, tileIdx, { pos }) } @@ -482,21 +518,21 @@ const moveTilesDiff = ( tileIdxs: Array, diff: Point ): void => { - const tileDrawSize = getTileDrawSize(gameId) + const drawSize = getPieceDrawSize(gameId) const bounds = getBounds(gameId) const cappedDiff = diff for (let tileIdx of tileIdxs) { - const t = getTile(gameId, tileIdx) + const t = getPiece(gameId, tileIdx) if (t.pos.x + diff.x < bounds.x) { cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x) - } else if (t.pos.x + tileDrawSize + diff.x > bounds.x + bounds.w) { - cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + tileDrawSize, cappedDiff.x) + } else if (t.pos.x + drawSize + diff.x > bounds.x + bounds.w) { + cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + drawSize, cappedDiff.x) } if (t.pos.y + diff.y < bounds.y) { cappedDiff.y = Math.max(bounds.y - t.pos.y, cappedDiff.y) - } else if (t.pos.y + tileDrawSize + diff.y > bounds.y + bounds.h) { - cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + tileDrawSize, cappedDiff.y) + } else if (t.pos.y + drawSize + diff.y > bounds.y + bounds.h) { + cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + drawSize, cappedDiff.y) } } @@ -522,52 +558,52 @@ const setTilesOwner = ( } // get all grouped tiles for a tile -function getGroupedTileIdxs(gameId: string, tileIdx: number): number[] { - const tiles = GAMES[gameId].puzzle.tiles - const tile = Util.decodeTile(tiles[tileIdx]) +function getGroupedPieceIdxs(gameId: string, pieceIdx: number): number[] { + const pieces = GAMES[gameId].puzzle.tiles + const piece = Util.decodePiece(pieces[pieceIdx]) const grouped = [] - if (tile.group) { - for (let other of tiles) { - const otherTile = Util.decodeTile(other) - if (otherTile.group === tile.group) { - grouped.push(otherTile.idx) + if (piece.group) { + for (let other of pieces) { + const otherPiece = Util.decodePiece(other) + if (otherPiece.group === piece.group) { + grouped.push(otherPiece.idx) } } } else { - grouped.push(tile.idx) + grouped.push(piece.idx) } return grouped } // Returns the index of the puzzle tile with the highest z index // that is not finished yet and that matches the position -const freeTileIdxByPos = (gameId: string, pos: Point): number => { +const freePieceIdxByPos = (gameId: string, pos: Point): number => { let info = GAMES[gameId].puzzle.info - let tiles = GAMES[gameId].puzzle.tiles + let pieces = GAMES[gameId].puzzle.tiles let maxZ = -1 - let tileIdx = -1 - for (let idx = 0; idx < tiles.length; idx++) { - const tile = Util.decodeTile(tiles[idx]) - if (tile.owner !== 0) { + let pieceIdx = -1 + for (let idx = 0; idx < pieces.length; idx++) { + const piece = Util.decodePiece(pieces[idx]) + if (piece.owner !== 0) { continue } const collisionRect: Rect = { - x: tile.pos.x, - y: tile.pos.y, + x: piece.pos.x, + y: piece.pos.y, w: info.tileSize, h: info.tileSize, } if (Geometry.pointInBounds(pos, collisionRect)) { - if (maxZ === -1 || tile.z > maxZ) { - maxZ = tile.z - tileIdx = idx + if (maxZ === -1 || piece.z > maxZ) { + maxZ = piece.z + pieceIdx = idx } } } - return tileIdx + return pieceIdx } const getPlayerBgColor = (gameId: string, playerId: string): string|null => { @@ -596,8 +632,8 @@ const areGrouped = ( tileIdx1: number, tileIdx2: number ): boolean => { - const g1 = getTileGroup(gameId, tileIdx1) - const g2 = getTileGroup(gameId, tileIdx2) + const g1 = getPieceGroup(gameId, tileIdx1) + const g2 = getPieceGroup(gameId, tileIdx2) return !!(g1 && g1 === g2) } @@ -643,7 +679,7 @@ function handleInput( const _tileChange = (tileIdx: number): void => { changes.push([ Protocol.CHANGE_TILE, - Util.encodeTile(getTile(gameId, tileIdx)), + Util.encodePiece(getPiece(gameId, tileIdx)), ]) } @@ -671,8 +707,8 @@ function handleInput( tileIdx2: number ): void => { const tiles = GAMES[gameId].puzzle.tiles - const group1 = getTileGroup(gameId, tileIdx1) - const group2 = getTileGroup(gameId, tileIdx2) + const group1 = getPieceGroup(gameId, tileIdx1) + const group2 = getPieceGroup(gameId, tileIdx2) let group const searchGroups = [] @@ -701,10 +737,10 @@ function handleInput( // TODO: strange if (searchGroups.length > 0) { for (const t of tiles) { - const tile = Util.decodeTile(t) - if (searchGroups.includes(tile.group)) { - changeTile(gameId, tile.idx, { group }) - _tileChange(tile.idx) + const piece = Util.decodePiece(t) + if (searchGroups.includes(piece.group)) { + changeTile(gameId, piece.idx, { group }) + _tileChange(piece.idx) } } } @@ -732,12 +768,12 @@ function handleInput( _playerChange() evtInfo._last_mouse_down = pos - const tileIdxAtPos = freeTileIdxByPos(gameId, pos) + const tileIdxAtPos = freePieceIdxByPos(gameId, pos) if (tileIdxAtPos >= 0) { let maxZ = getMaxZIndex(gameId) + 1 changeData(gameId, { maxZ }) _dataChange() - const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos) + const tileIdxs = getGroupedPieceIdxs(gameId, tileIdxAtPos) setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId)) setTilesOwner(gameId, tileIdxs, playerId) _tileChanges(tileIdxs) @@ -754,7 +790,7 @@ function handleInput( changePlayer(gameId, playerId, {x, y, ts}) _playerChange() } else { - let tileIdx = getFirstOwnedTileIdx(gameId, playerId) + let tileIdx = getFirstOwnedPieceIdx(gameId, playerId) if (tileIdx >= 0) { // player is moving a tile (and hand) changePlayer(gameId, playerId, {x, y, ts}) @@ -762,11 +798,11 @@ function handleInput( // check if pos is on the tile, otherwise dont move // (mouse could be out of table, but tile stays on it) - const tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + const tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) let anyOk = Geometry.pointInBounds(pos, getBounds(gameId)) && Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId)) for (let idx of tileIdxs) { - const bounds = getTileBounds(gameId, idx) + const bounds = getPieceBounds(gameId, idx) if (Geometry.pointInBounds(pos, bounds)) { anyOk = true break @@ -799,16 +835,16 @@ function handleInput( evtInfo._last_mouse_down = null - let tileIdx = getFirstOwnedTileIdx(gameId, playerId) + let tileIdx = getFirstOwnedPieceIdx(gameId, playerId) if (tileIdx >= 0) { // drop the tile(s) - let tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) setTilesOwner(gameId, tileIdxs, 0) _tileChanges(tileIdxs) // Check if the tile was dropped near the final location - let tilePos = getTilePos(gameId, tileIdx) - let finalPos = getFinalTilePos(gameId, tileIdx) + let tilePos = getPiecePos(gameId, tileIdx) + let finalPos = getFinalPiecePos(gameId, tileIdx) if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) { let diff = Geometry.pointSub(finalPos, tilePos) // Snap the tile to the final destination @@ -829,7 +865,7 @@ function handleInput( _playerChange() // check if the puzzle is finished - if (getFinishedTileCount(gameId) === getTileCount(gameId)) { + if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) { changeData(gameId, { finished: ts }) _dataChange() } @@ -848,17 +884,17 @@ function handleInput( if (areGrouped(gameId, tileIdx, otherTileIdx)) { return false } - const tilePos = getTilePos(gameId, tileIdx) + const tilePos = getPiecePos(gameId, tileIdx) const dstPos = Geometry.pointAdd( - getTilePos(gameId, otherTileIdx), + getPiecePos(gameId, otherTileIdx), {x: off[0] * info.tileSize, y: off[1] * info.tileSize} ) if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) { let diff = Geometry.pointSub(dstPos, tilePos) - let tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) moveTilesDiff(gameId, tileIdxs, diff) groupTiles(gameId, tileIdx, otherTileIdx) - tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs) setTilesZIndex(gameId, tileIdxs, zIndex) _tileChanges(tileIdxs) @@ -868,7 +904,7 @@ function handleInput( } let snapped = false - for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) { + for (let tileIdxTmp of getGroupedPieceIdxs(gameId, tileIdx)) { let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp) if ( check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top @@ -916,15 +952,14 @@ function handleInput( } export default { - __createPlayerObject, setGame, exists, playerExists, getActivePlayers, getIdlePlayers, addPlayer, - getFinishedTileCount, - getTileCount, + getFinishedPiecesCount, + getPieceCount, getImageUrl, setImageUrl, get, @@ -936,7 +971,7 @@ export default { getPlayerIdByIndex, changePlayer, setPlayer, - setTile, + setPiece, setPuzzleData, getTableWidth, getTableHeight, @@ -944,11 +979,11 @@ export default { getRng, getPuzzleWidth, getPuzzleHeight, - getTilesSortedByZIndex, - getFirstOwnedTile, - getTileDrawOffset, - getTileDrawSize, - getFinalTilePos, + getPiecesSortedByZIndex, + getFirstOwnedPiece, + getPieceDrawOffset, + getPieceDrawSize, + getFinalPiecePos, getStartTs, getFinishTs, handleInput, diff --git a/src/common/Rng.ts b/src/common/Rng.ts index e18f80c..af72dfc 100644 --- a/src/common/Rng.ts +++ b/src/common/Rng.ts @@ -1,4 +1,4 @@ -interface RngSerialized { +export interface RngSerialized { rand_high: number, rand_low: number, } diff --git a/src/common/Types.ts b/src/common/Types.ts new file mode 100644 index 0000000..279f5f7 --- /dev/null +++ b/src/common/Types.ts @@ -0,0 +1,6 @@ +// @see https://stackoverflow.com/a/59906630/392905 +type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number +type ArrayItems> = T extends Array ? TItems : never +export type FixedLengthArray = + Pick> + & { [Symbol.iterator]: () => IterableIterator< ArrayItems > } diff --git a/src/common/Util.ts b/src/common/Util.ts index f691b75..c988b9d 100644 --- a/src/common/Util.ts +++ b/src/common/Util.ts @@ -1,4 +1,14 @@ -import { EncodedPiece, EncodedPieceShape, EncodedPlayer, Piece, PieceShape, Player } from './GameCommon' +import { + EncodedGame, + EncodedPiece, + EncodedPieceShape, + EncodedPlayer, + Game, + Piece, + PieceShape, + Player, + ScoreMode +} from './GameCommon' import { Point } from './Geometry' import { Rng } from './Rng' @@ -58,11 +68,11 @@ function decodeShape(data: EncodedPieceShape): PieceShape { } } -function encodeTile(data: Piece): EncodedPiece { +function encodePiece(data: Piece): EncodedPiece { return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group] } -function decodeTile(data: EncodedPiece): Piece { +function decodePiece(data: EncodedPiece): Piece { return { idx: data[0], pos: { @@ -103,25 +113,19 @@ function decodePlayer(data: EncodedPlayer): Player { } } -function encodeGame(data: any): Array { - if (Array.isArray(data)) { - return data - } +function encodeGame(data: Game): EncodedGame { return [ data.id, - data.rng.type, + data.rng.type || '', Rng.serialize(data.rng.obj), data.puzzle, data.players, data.evtInfos, - data.scoreMode, + data.scoreMode || ScoreMode.FINAL, ] } -function decodeGame(data: any) { - if (!Array.isArray(data)) { - return data - } +function decodeGame(data: EncodedGame): Game { return { id: data[0], rng: { @@ -174,8 +178,8 @@ export default { encodeShape, decodeShape, - encodeTile, - decodeTile, + encodePiece, + decodePiece, encodePlayer, decodePlayer, diff --git a/src/frontend/Communication.ts b/src/frontend/Communication.ts index 7b62fff..d84e629 100644 --- a/src/frontend/Communication.ts +++ b/src/frontend/Communication.ts @@ -1,6 +1,7 @@ "use strict" -import { logger } from '../common/Util' +import { EncodedGame, ReplayData } from '../common/GameCommon' +import Util, { logger } from '../common/Util' import Protocol from './../common/Protocol' const log = logger('Communication.js') @@ -51,7 +52,7 @@ function connect( address: string, gameId: string, clientId: string -): Promise { +): Promise { clientSeq = 0 events = {} setConnectionState(CONN_STATE_CONNECTING) @@ -100,9 +101,10 @@ async function requestReplayData( gameId: string, offset: number, size: number -): Promise<{ log: Array, game: any }> { - const res = await fetch(`/api/replay-data?gameId=${gameId}&offset=${offset}&size=${size}`) - const json: { log: Array, game: any } = await res.json() +): Promise { + const args = { gameId, offset, size } + const res = await fetch(`/api/replay-data${Util.asQueryArgs(args)}`) + const json: ReplayData = await res.json() return json } diff --git a/src/frontend/PuzzleGraphics.ts b/src/frontend/PuzzleGraphics.ts index 52f37ad..c4c7716 100644 --- a/src/frontend/PuzzleGraphics.ts +++ b/src/frontend/PuzzleGraphics.ts @@ -94,7 +94,7 @@ async function createPuzzleTileBitmaps( const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D for (const t of tiles) { - const tile = Util.decodeTile(t) + const tile = Util.decodePiece(t) const srcRect = srcRectByIdx(info, tile.idx) const path = pathForShape(Util.decodeShape(info.shapes[tile.idx])) diff --git a/src/frontend/game.ts b/src/frontend/game.ts index cd69b1a..1857908 100644 --- a/src/frontend/game.ts +++ b/src/frontend/game.ts @@ -7,11 +7,12 @@ import Debug from './Debug' import Communication from './Communication' import Util from './../common/Util' import PuzzleGraphics from './PuzzleGraphics' -import Game, { Player, Piece } from './../common/GameCommon' +import Game, { Game as GameType, Player, Piece, EncodedGame, ReplayData, Timestamp } from './../common/GameCommon' import fireworksController from './Fireworks' import Protocol from '../common/Protocol' import Time from '../common/Time' import { Dim, Point } from '../common/Geometry' +import { FixedLengthArray } from '../common/Types' declare global { interface Window { @@ -19,13 +20,6 @@ declare global { } } -// @see https://stackoverflow.com/a/59906630/392905 -type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number -type ArrayItems> = T extends Array ? TItems : never -type FixedLengthArray = - Pick> - & { [Symbol.iterator]: () => IterableIterator< ArrayItems > } - // @ts-ignore const images = import.meta.globEager('./*.png') @@ -50,15 +44,17 @@ interface Hud { interface Replay { final: boolean requesting: boolean - log: Array - logPointer: number, - logIdx: number + log: Array // current log entries + logPointer: number // pointer to current item in the log array speeds: Array speedIdx: number paused: boolean lastRealTs: number lastGameTs: number gameStartTs: number + // + dataOffset: number + dataSize: number } const shouldDrawPiece = (piece: Piece) => { @@ -267,51 +263,61 @@ export async function main( requesting: true, log: [], logPointer: 0, - logIdx: 0, speeds: [0.5, 1, 2, 5, 10, 20, 50, 100, 250, 500], speedIdx: 1, paused: false, lastRealTs: 0, lastGameTs: 0, gameStartTs: 0, + dataOffset: 0, + dataSize: 10000, } Communication.onConnectionStateChange((state) => { HUD.setConnectionState(state) }) + const queryNextReplayBatch = async ( + gameId: string + ): Promise => { + REPLAY.requesting = true + const replay: ReplayData = await Communication.requestReplayData( + gameId, + REPLAY.dataOffset, + REPLAY.dataSize + ) + REPLAY.dataOffset += REPLAY.dataSize + REPLAY.requesting = false + return replay + } + const getNextReplayBatch = async ( - gameId: string, - offset: number, - size: number + gameId: string ) => { - const replay: { - game: any, - log: Array - } = await Communication.requestReplayData(gameId, offset, size) + const replay: ReplayData = await queryNextReplayBatch(gameId) // cut log that was already handled REPLAY.log = REPLAY.log.slice(REPLAY.logPointer) REPLAY.logPointer = 0 REPLAY.log.push(...replay.log) - if (replay.log.length < 10000) { + if (replay.log.length < REPLAY.dataSize) { REPLAY.final = true } - REPLAY.requesting = false } + let TIME: () => number = () => 0 const connect = async () => { if (MODE === MODE_PLAY) { - const game = await Communication.connect(wsAddress, gameId, clientId) - const gameObject = Util.decodeGame(game) + const game: EncodedGame = await Communication.connect(wsAddress, gameId, clientId) + const gameObject: GameType = Util.decodeGame(game) Game.setGame(gameObject.id, gameObject) TIME = () => Time.timestamp() } else if (MODE === MODE_REPLAY) { - const replay: { - game: any, - log: Array - } = await Communication.requestReplayData(gameId, 0, 10000) - const gameObject = Util.decodeGame(replay.game) + const replay: ReplayData = await queryNextReplayBatch(gameId) + if (!replay.game) { + throw '[ 2021-05-29 no game received ]' + } + const gameObject: GameType = Util.decodeGame(replay.game) Game.setGame(gameObject.id, gameObject) REPLAY.requesting = false REPLAY.log = replay.log @@ -329,8 +335,8 @@ export async function main( await connect() - const TILE_DRAW_OFFSET = Game.getTileDrawOffset(gameId) - const TILE_DRAW_SIZE = Game.getTileDrawSize(gameId) + const PIECE_DRAW_OFFSET = Game.getPieceDrawOffset(gameId) + const PIECE_DRAW_SIZE = Game.getPieceDrawSize(gameId) const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId) const PUZZLE_HEIGHT = Game.getPuzzleHeight(gameId) const TABLE_WIDTH = Game.getTableWidth(gameId) @@ -345,8 +351,8 @@ export async function main( h: PUZZLE_HEIGHT, } const PIECE_DIM = { - w: TILE_DRAW_SIZE, - h: TILE_DRAW_SIZE, + w: PIECE_DRAW_SIZE, + h: PIECE_DRAW_SIZE, } const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId)) @@ -380,8 +386,8 @@ export async function main( } updateTimerElements() - HUD.setPiecesDone(Game.getFinishedTileCount(gameId)) - HUD.setPiecesTotal(Game.getTileCount(gameId)) + HUD.setPiecesDone(Game.getFinishedPiecesCount(gameId)) + HUD.setPiecesTotal(Game.getPieceCount(gameId)) const ts = TIME() HUD.setActivePlayers(Game.getActivePlayers(gameId, ts)) HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts)) @@ -470,8 +476,8 @@ export async function main( } } break; case Protocol.CHANGE_TILE: { - const t = Util.decodeTile(changeData) - Game.setTile(gameId, t.idx, t) + const t = Util.decodePiece(changeData) + Game.setPiece(gameId, t.idx, t) RERENDER = true } break; case Protocol.CHANGE_DATA: { @@ -494,8 +500,7 @@ export async function main( if (REPLAY.logPointer + 1 >= REPLAY.log.length) { REPLAY.lastRealTs = realTs - REPLAY.requesting = true - getNextReplayBatch(gameId, REPLAY.logIdx, 10000) + getNextReplayBatch(gameId) return } @@ -519,7 +524,7 @@ export async function main( } const logEntry = REPLAY.log[nextIdx] - const nextTs = REPLAY.gameStartTs + logEntry[logEntry.length - 1] + const nextTs: Timestamp = REPLAY.gameStartTs + logEntry[logEntry.length - 1] if (nextTs > maxGameTs) { break } @@ -546,7 +551,6 @@ export async function main( RERENDER = true } REPLAY.logPointer = nextIdx - REPLAY.logIdx++ } while (true) REPLAY.lastRealTs = realTs REPLAY.lastGameTs = maxGameTs @@ -572,7 +576,7 @@ export async function main( RERENDER = true viewport.move(diffX, diffY) } else if (type === Protocol.INPUT_EV_MOUSE_MOVE) { - if (_last_mouse_down && !Game.getFirstOwnedTile(gameId, clientId)) { + if (_last_mouse_down && !Game.getFirstOwnedPiece(gameId, clientId)) { // move the cam const pos = { x: evt[1], y: evt[2] } const mouse = viewport.worldToViewport(pos) @@ -692,7 +696,7 @@ export async function main( // DRAW TILES // --------------------------------------------------------------- - const tiles = Game.getTilesSortedByZIndex(gameId) + const tiles = Game.getPiecesSortedByZIndex(gameId) if (window.DEBUG) Debug.checkpoint('get tiles done') dim = viewport.worldDimToViewportRaw(PIECE_DIM) @@ -702,8 +706,8 @@ export async function main( } bmp = bitmaps[tile.idx] pos = viewport.worldToViewportRaw({ - x: TILE_DRAW_OFFSET + tile.pos.x, - y: TILE_DRAW_OFFSET + tile.pos.y, + x: PIECE_DRAW_OFFSET + tile.pos.x, + y: PIECE_DRAW_OFFSET + tile.pos.y, }) ctx.drawImage(bmp, 0, 0, bmp.width, bmp.height, @@ -743,7 +747,7 @@ export async function main( // --------------------------------------------------------------- HUD.setActivePlayers(Game.getActivePlayers(gameId, ts)) HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts)) - HUD.setPiecesDone(Game.getFinishedTileCount(gameId)) + HUD.setPiecesDone(Game.getFinishedPiecesCount(gameId)) if (window.DEBUG) Debug.checkpoint('HUD done') // --------------------------------------------------------------- diff --git a/src/server/Game.ts b/src/server/Game.ts index e6cd689..50c17aa 100644 --- a/src/server/Game.ts +++ b/src/server/Game.ts @@ -1,18 +1,18 @@ -import GameCommon, { ScoreMode } from './../common/GameCommon' +import GameCommon, { Game, ScoreMode, Timestamp } from './../common/GameCommon' import Util from './../common/Util' import { Rng } from './../common/Rng' import GameLog from './GameLog' -import { createPuzzle } from './Puzzle' +import { createPuzzle, PuzzleCreationImageInfo } from './Puzzle' import Protocol from './../common/Protocol' import GameStorage from './GameStorage' async function createGameObject( gameId: string, targetTiles: number, - image: { file: string, url: string }, + image: PuzzleCreationImageInfo, ts: number, scoreMode: ScoreMode -) { +): Promise { const seed = Util.hash(gameId + ' ' + ts) const rng = new Rng(seed) return { @@ -28,11 +28,17 @@ async function createGameObject( async function createGame( gameId: string, targetTiles: number, - image: { file: string, url: string }, + image: PuzzleCreationImageInfo, ts: number, scoreMode: ScoreMode ): Promise { - const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode) + const gameObject = await createGameObject( + gameId, + targetTiles, + image, + ts, + scoreMode + ) GameLog.create(gameId) GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode) @@ -41,7 +47,7 @@ async function createGame( GameStorage.setDirty(gameId) } -function addPlayer(gameId: string, playerId: string, ts: number): void { +function addPlayer(gameId: string, playerId: string, ts: Timestamp): void { const idx = GameCommon.getPlayerIndexById(gameId, playerId) const diff = ts - GameCommon.getStartTs(gameId) if (idx === -1) { @@ -74,14 +80,4 @@ export default { createGame, addPlayer, handleInput, - getAllGames: GameCommon.getAllGames, - getActivePlayers: GameCommon.getActivePlayers, - getFinishedTileCount: GameCommon.getFinishedTileCount, - getImageUrl: GameCommon.getImageUrl, - getTileCount: GameCommon.getTileCount, - exists: GameCommon.exists, - playerExists: GameCommon.playerExists, - get: GameCommon.get, - getStartTs: GameCommon.getStartTs, - getFinishTs: GameCommon.getFinishTs, } diff --git a/src/server/GameLog.ts b/src/server/GameLog.ts index 7fb614a..ca1d306 100644 --- a/src/server/GameLog.ts +++ b/src/server/GameLog.ts @@ -8,19 +8,19 @@ const log = logger('GameLog.js') const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log` -const create = (gameId: string) => { +const create = (gameId: string): void => { const file = filename(gameId) if (!fs.existsSync(file)) { fs.appendFileSync(file, '') } } -const exists = (gameId: string) => { +const exists = (gameId: string): boolean => { const file = filename(gameId) return fs.existsSync(file) } -const _log = (gameId: string, ...args: Array) => { +const _log = (gameId: string, ...args: Array): void => { const file = filename(gameId) if (!fs.existsSync(file)) { return diff --git a/src/server/GameStorage.ts b/src/server/GameStorage.ts index 91a50eb..52357c8 100644 --- a/src/server/GameStorage.ts +++ b/src/server/GameStorage.ts @@ -41,7 +41,7 @@ function loadGame(gameId: string): void { } if (typeof game.puzzle.data.finished === 'undefined') { const unfinished = game.puzzle.tiles - .map(Util.decodeTile) + .map(Util.decodePiece) .find((t: Piece) => t.owner !== -1) game.puzzle.data.finished = unfinished ? 0 : Time.timestamp() } diff --git a/src/server/Puzzle.ts b/src/server/Puzzle.ts index d3eb518..e800977 100644 --- a/src/server/Puzzle.ts +++ b/src/server/Puzzle.ts @@ -2,7 +2,12 @@ import Util from './../common/Util' import { Rng } from './../common/Rng' import Images from './Images' import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle } from '../common/GameCommon' -import { Point } from '../common/Geometry' +import { Dim, Point } from '../common/Geometry' + +export interface PuzzleCreationImageInfo { + file: string + url: string +} interface PuzzleCreationInfo { width: number @@ -22,7 +27,7 @@ const TILE_SIZE = 64 async function createPuzzle( rng: Rng, targetTiles: number, - image: { file: string, url: string }, + image: PuzzleCreationImageInfo, ts: number ): Promise { const imagePath = image.file @@ -33,11 +38,7 @@ async function createPuzzle( if (!dim.w || !dim.h) { throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]` } - const info: PuzzleCreationInfo = determinePuzzleInfo( - dim.w, - dim.h, - targetTiles - ) + const info: PuzzleCreationInfo = determinePuzzleInfo(dim, targetTiles) let tiles = new Array(info.tiles) for (let i = 0; i < tiles.length; i++) { @@ -98,7 +99,7 @@ async function createPuzzle( positions = rng.shuffle(positions) const pieces: Array = tiles.map(tile => { - return Util.encodeTile({ + return Util.encodePiece({ idx: tile.idx, // index of tile in the array group: 0, // if grouped with other tiles z: 0, // z index of the tile @@ -181,9 +182,12 @@ function determinePuzzleTileShapes( return shapes.map(Util.encodeShape) } -const determineTilesXY = (w: number, h: number, targetTiles: number) => { - const w_ = w < h ? (w * h) : (w * w) - const h_ = w < h ? (h * h) : (w * h) +const determineTilesXY = ( + dim: Dim, + targetTiles: number +): { tilesX: number, tilesY: number } => { + const w_ = dim.w < dim.h ? (dim.w * dim.h) : (dim.w * dim.w) + const h_ = dim.w < dim.h ? (dim.h * dim.h) : (dim.w * dim.h) let size = 0 let tiles = 0 do { @@ -198,11 +202,10 @@ const determineTilesXY = (w: number, h: number, targetTiles: number) => { } const determinePuzzleInfo = ( - w: number, - h: number, + dim: Dim, targetTiles: number ): PuzzleCreationInfo => { - const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles) + const {tilesX, tilesY} = determineTilesXY(dim, targetTiles) const tiles = tilesX * tilesY const tileSize = TILE_SIZE const width = tilesX * tileSize diff --git a/src/server/main.ts b/src/server/main.ts index a25ece8..df2e599 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -19,7 +19,7 @@ import { PUBLIC_DIR, UPLOAD_DIR, } from './Dirs' -import { GameSettings, ScoreMode } from '../common/GameCommon' +import GameCommon, { Game as GameType, GameSettings, ScoreMode } from '../common/GameCommon' import GameStorage from './GameStorage' import Db from './Db' @@ -81,7 +81,7 @@ app.get('/api/replay-data', async (req, res) => { return } const log = await GameLog.get(gameId, offset, size) - let game = null + let game: GameType|null = null if (offset === 0) { // also need the game game = await Game.createGameObject( @@ -107,15 +107,15 @@ app.get('/api/newgame-data', (req, res) => { app.get('/api/index-data', (req, res) => { const ts = Time.timestamp() const games = [ - ...Game.getAllGames().map((game: any) => ({ + ...GameCommon.getAllGames().map((game: any) => ({ id: game.id, hasReplay: GameLog.exists(game.id), - started: Game.getStartTs(game.id), - finished: Game.getFinishTs(game.id), - tilesFinished: Game.getFinishedTileCount(game.id), - tilesTotal: Game.getTileCount(game.id), - players: Game.getActivePlayers(game.id, ts).length, - imageUrl: Game.getImageUrl(game.id), + started: GameCommon.getStartTs(game.id), + finished: GameCommon.getFinishTs(game.id), + tilesFinished: GameCommon.getFinishedPiecesCount(game.id), + tilesTotal: GameCommon.getPieceCount(game.id), + players: GameCommon.getActivePlayers(game.id, ts).length, + imageUrl: GameCommon.getImageUrl(game.id), })), ] @@ -193,7 +193,7 @@ app.post('/newgame', bodyParser.json(), async (req, res) => { const gameSettings = req.body as GameSettings log.log(gameSettings) const gameId = Util.uniqId() - if (!Game.exists(gameId)) { + if (!GameCommon.exists(gameId)) { const ts = Time.timestamp() await Game.createGame( gameId, @@ -238,13 +238,13 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => { const msgType = msg[0] switch (msgType) { case Protocol.EV_CLIENT_INIT: { - if (!Game.exists(gameId)) { + if (!GameCommon.exists(gameId)) { throw `[game ${gameId} does not exist... ]` } const ts = Time.timestamp() Game.addPlayer(gameId, clientId, ts) GameSockets.addSocket(gameId, socket) - const game = Game.get(gameId) + const game: GameType = GameCommon.get(gameId) notify( [Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [socket] @@ -252,7 +252,7 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => { } break case Protocol.EV_CLIENT_EVENT: { - if (!Game.exists(gameId)) { + if (!GameCommon.exists(gameId)) { throw `[game ${gameId} does not exist... ]` } const clientSeq = msg[1] @@ -260,7 +260,7 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => { const ts = Time.timestamp() let sendGame = false - if (!Game.playerExists(gameId, clientId)) { + if (!GameCommon.playerExists(gameId, clientId)) { Game.addPlayer(gameId, clientId, ts) sendGame = true } @@ -269,7 +269,7 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => { sendGame = true } if (sendGame) { - const game = Game.get(gameId) + const game: GameType = GameCommon.get(gameId) notify( [Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [socket]