added leaderboard (closes #29)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
const Popup=require('popup');
|
||||
const levels=require('levels');
|
||||
const config=require('config');
|
||||
|
||||
const upload=async (mode, win, snek) => {
|
||||
if(!win && !snek.rules.uploadOnDeath) return;
|
||||
if(window.serverless) return;
|
||||
|
||||
const username=config.getS('player.name');
|
||||
const score=snek.score;
|
||||
const length=snek.length;
|
||||
const time=snek.endPlayTime;
|
||||
const speed=snek.speed;
|
||||
|
||||
const rst=await fetch('api/leaderboards/'+mode, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
score, length,
|
||||
time, speed
|
||||
})
|
||||
});
|
||||
const dat=await rst.json();
|
||||
if(!dat.ok) console.error(rst.err);
|
||||
};
|
||||
|
||||
const show=async (mode='speedrun/1', page=1) => {
|
||||
let popup=new Popup("Leaderboards: "+mode);
|
||||
|
||||
const [category, id]=mode.split('/');
|
||||
let modes=[];
|
||||
(() => {
|
||||
Object.keys(window.levelList).forEach(cat => {
|
||||
window.levelList[cat].levels.forEach(lvl => {
|
||||
modes.push(cat+'/'+lvl);
|
||||
});
|
||||
});
|
||||
})();
|
||||
const prevMode=() => {
|
||||
let idx=modes.indexOf(mode);
|
||||
return modes[idx-1]||modes[modes.length-1];
|
||||
};
|
||||
const nextMode=() => {
|
||||
let idx=modes.indexOf(mode);
|
||||
return modes[idx+1]||modes[0];
|
||||
};
|
||||
|
||||
const rules=await levels.getRules(category, id);
|
||||
const sort=rules.leaderboardsSort;
|
||||
const rst=await fetch('api/leaderboards/'+mode+'?sort='+sort+'&page='+page+'&results=10');
|
||||
const {ok, data, err}=await rst.json();
|
||||
|
||||
popup.buttons.close="Close";
|
||||
popup.buttons.modeP="Previous mode";
|
||||
popup.buttons.modeN="Next mode";
|
||||
popup.large=true;
|
||||
popup.animation=false;
|
||||
|
||||
if(ok) {
|
||||
popup.addStrong("Page "+page);
|
||||
if(data.length==10) popup.buttons.next="Next page";
|
||||
if(page>1) popup.buttons.prev="Previous page";
|
||||
|
||||
if(data.length==0) {
|
||||
popup.addEm("No data");
|
||||
} else {
|
||||
const rpad=(n, digits=2, pad=' ') =>
|
||||
((''+n).length>=digits)?(''+n):(rpad(pad+n, digits, pad));
|
||||
|
||||
popup.addTable(data.map(({username, score, length, speed, time}, i) => {
|
||||
return {
|
||||
rank: '#'+(i+(page-1)*10+1),
|
||||
username,
|
||||
score: score+'pts',
|
||||
length,
|
||||
speed: speed+'tps',
|
||||
time: rpad(Math.floor(time/60000), 2, '0')+
|
||||
':'+rpad(Math.floor(time/1000)%60, 2, '0')+
|
||||
':'+rpad(time%1000, 3, '0')
|
||||
};
|
||||
}), [
|
||||
'rank',
|
||||
'username',
|
||||
'score',
|
||||
'length',
|
||||
'speed',
|
||||
'time'
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
popup.addStrong("Error loading leaderboards");
|
||||
popup.addEm(err);
|
||||
}
|
||||
|
||||
Popup.dismiss();
|
||||
const verb=await popup.display();
|
||||
if(verb=='next') return show(mode, page+1);
|
||||
else if(verb=='prev') return show(mode, page-1);
|
||||
else if(verb=='modeP') return show(prevMode());
|
||||
else if(verb=='modeN') return show(nextMode());
|
||||
location.hash='';
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
upload, show
|
||||
};
|
||||
+26
-1
@@ -7,12 +7,37 @@ const get=async filename => {
|
||||
return cache[filename]=json;
|
||||
};
|
||||
|
||||
const getInfo=(category, id) => {
|
||||
const cat=levelList[category];
|
||||
id=''+id;
|
||||
|
||||
const displayName=cat.levelDisplay
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const fileName=cat.levelFilename
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const levelString=category+'/'+id+'/'+fileName;
|
||||
|
||||
return {
|
||||
displayName,
|
||||
fileName,
|
||||
levelString
|
||||
};
|
||||
};
|
||||
|
||||
const getRules=async (category, id) => {
|
||||
const {fileName}=getInfo(category, id);
|
||||
const json=await get(fileName);
|
||||
return Object.assign({}, window.levelList[category].rules, json.rules);
|
||||
};
|
||||
|
||||
const clearCache=() =>
|
||||
Object
|
||||
.keys(cache)
|
||||
.forEach(key => delete cache[key]);
|
||||
|
||||
return module.exports={
|
||||
get,
|
||||
get, getRules, getInfo,
|
||||
clearCache
|
||||
};
|
||||
|
||||
+37
-25
@@ -8,6 +8,7 @@
|
||||
const input=require('input');
|
||||
const levels=require('levels');
|
||||
const config=require('config');
|
||||
const leaderboards=require('leaderboards');
|
||||
|
||||
// get a known state
|
||||
await new Promise(ok => assets.onReady(ok));
|
||||
@@ -20,13 +21,29 @@
|
||||
const hud=main.querySelector('#hud');
|
||||
|
||||
// load data from server
|
||||
const levelList=assets.get('levelList');
|
||||
const levelList=window.levelList=assets.get('levelList');
|
||||
|
||||
// detect if we're running with a server
|
||||
const serverless=window.serverless=await (async() => {
|
||||
const res=await fetch('api/has-nodejs');
|
||||
if(!res.ok) return true;
|
||||
const msg=await res.json();
|
||||
return msg!='yes';
|
||||
})();
|
||||
if(serverless) {
|
||||
document.body.classList.add('serverless');
|
||||
} else {
|
||||
document.body.classList.add('server');
|
||||
}
|
||||
|
||||
// flag the body as loaded
|
||||
document.body.classList.remove('loading');
|
||||
|
||||
// get our global variables
|
||||
let currentGame=null;
|
||||
|
||||
// forward-declare functions
|
||||
let resizeCanvas, getLevel, startGame, stopGame, handleWin, handleDeath, menu, help, settings, restart, updateHud;
|
||||
let resizeCanvas, startGame, stopGame, handleWin, handleDeath, menu, help, settings, showLeaderboards, restart, updateHud;
|
||||
|
||||
// handle window resize and fullscreen
|
||||
resizeCanvas=() => {
|
||||
@@ -48,26 +65,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// get a level for a category and an id
|
||||
getLevel=(category, id) => {
|
||||
const cat=levelList[category];
|
||||
id=''+id;
|
||||
|
||||
const displayName=cat.levelDisplay
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const fileName=cat.levelFilename
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const levelString=category+'/'+id+'/'+fileName;
|
||||
|
||||
return {
|
||||
displayName,
|
||||
fileName,
|
||||
levelString
|
||||
};
|
||||
};
|
||||
|
||||
// buid menu from level list
|
||||
Object.keys(levelList).forEach(category => {
|
||||
const cat=levelList[category];
|
||||
@@ -81,7 +78,7 @@
|
||||
|
||||
const ul=section.appendChild(document.createElement('ul'));
|
||||
cat.levels.forEach((level, i) => {
|
||||
const {displayName, fileName, levelString}=getLevel(category, level);
|
||||
const {displayName, fileName, levelString}=levels.getInfo(category, level);
|
||||
const li=ul.appendChild(document.createElement('li'));
|
||||
const a=li.appendChild(document.createElement('a'));
|
||||
a.href='#'+levelString;
|
||||
@@ -106,6 +103,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
// display the leaderboards
|
||||
showLeaderboards=() => {
|
||||
stopGame();
|
||||
leaderboards.show();
|
||||
};
|
||||
|
||||
// start a new game
|
||||
startGame=async (category, levelId, filename) => {
|
||||
// stop any running games and clear popups
|
||||
@@ -192,6 +195,9 @@
|
||||
// fetch userdata from the game
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
|
||||
// upload scores
|
||||
if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, true, snek);
|
||||
|
||||
// create and configure popup
|
||||
let popup=new Popup("Finished!");
|
||||
popup.addStrong("You won!");
|
||||
@@ -221,7 +227,7 @@
|
||||
} else if(result=='next') {
|
||||
const {category, levelId}=snek.userdata;
|
||||
let nextId=(+levelId)+1;
|
||||
let {levelString}=getLevel(category, nextId)
|
||||
let {levelString}=levels.getInfo(category, nextId)
|
||||
location.hash=levelString;
|
||||
}
|
||||
};
|
||||
@@ -231,6 +237,12 @@
|
||||
// hide the HUD
|
||||
hud.classList.add('hidden');
|
||||
|
||||
// fetch userdata from the game
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
|
||||
// upload scores
|
||||
if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, false, snek);
|
||||
|
||||
// create and configure popup
|
||||
let popup=new Popup("Finished!");
|
||||
popup.addStrong(config.getS('player.name')+' '+snek.death.message);
|
||||
@@ -251,7 +263,6 @@
|
||||
|
||||
// act on it
|
||||
if(result=='retry') {
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
startGame(category, levelId, filename);
|
||||
} else if(result=='menu') {
|
||||
location.hash='menu';
|
||||
@@ -304,6 +315,7 @@
|
||||
if(hash=='' || hash=='menu') return menu();
|
||||
else if(hash=='help') return help();
|
||||
else if(hash=='settings') return settings();
|
||||
else if(hash=='leaderboards') return showLeaderboards();
|
||||
|
||||
const [_, category, levelId, filename]=location.hash.match(/([a-zA-Z0-9_-]+?)\/([a-zA-Z0-9_-]+?)\/(.+)/);
|
||||
startGame(category, levelId, filename);
|
||||
|
||||
@@ -17,6 +17,7 @@ const objToDom=obj => {
|
||||
return ul;
|
||||
} else {
|
||||
let table=document.createElement('table');
|
||||
table.classList.add('dual');
|
||||
Object
|
||||
.keys(obj)
|
||||
.forEach(key => {
|
||||
@@ -34,6 +35,7 @@ class Popup {
|
||||
this.content=content.map(objToDom);
|
||||
this.buttons={...buttons};
|
||||
this.large=large;
|
||||
this.animation=true;
|
||||
}
|
||||
|
||||
addContent(cnt) {
|
||||
@@ -53,6 +55,25 @@ class Popup {
|
||||
hn.innerText=cnt;
|
||||
this.content.push(hn);
|
||||
}
|
||||
addTable(data, heading=Object.keys(data)) {
|
||||
let table=document.createElement('table');
|
||||
table.classList.add('table');
|
||||
let thead=table.appendChild(document.createElement('thead'));
|
||||
let headingRow=thead.appendChild(document.createElement('tr'));
|
||||
heading.forEach(key => {
|
||||
let th=headingRow.appendChild(document.createElement('th'));
|
||||
th.innerText=key;
|
||||
});
|
||||
let tbody=table.appendChild(document.createElement('tbody'));
|
||||
data.forEach(row => {
|
||||
let tr=tbody.appendChild(document.createElement('tr'));
|
||||
heading.forEach(key => {
|
||||
let td=tr.appendChild(document.createElement('td'));
|
||||
td.innerText=row[key];
|
||||
});
|
||||
});
|
||||
this.content.push(table);
|
||||
}
|
||||
|
||||
async display(parent=document.body) {
|
||||
let outer=document.createElement('div');
|
||||
@@ -60,6 +81,7 @@ class Popup {
|
||||
let popup=outer.appendChild(document.createElement('div'));
|
||||
popup.classList.add('content');
|
||||
if(this.large) popup.classList.add('large');
|
||||
if(this.animation) outer.classList.add('animation');
|
||||
|
||||
let title=popup.appendChild(document.createElement('h1'));
|
||||
title.innerText=this.title;
|
||||
|
||||
+14
-2
@@ -8,7 +8,10 @@
|
||||
}
|
||||
|
||||
.popup {
|
||||
animation: popupAppear 1s linear;
|
||||
&.animation {
|
||||
animation: popupAppear 1s linear;
|
||||
}
|
||||
|
||||
background: rgba(0, 0, 0, 90%);
|
||||
|
||||
position: absolute;
|
||||
@@ -64,7 +67,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
.dual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -86,6 +89,15 @@
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
td, th {
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
@@ -129,6 +129,16 @@ p {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.serverless .server {
|
||||
display: none !important;
|
||||
}
|
||||
body.server .serverless {
|
||||
display: none !important;
|
||||
}
|
||||
body.loading .loaded {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// setup the progress bar
|
||||
@import 'progressBar';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user