added leaderboard (closes #29)

This commit is contained in:
Nathan DECHER
2020-04-13 21:58:53 +02:00
parent 1ca8e46461
commit 7b083fda11
20 changed files with 852 additions and 48 deletions
+109
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+22
View File
@@ -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
View File
@@ -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;
}
+10
View File
@@ -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';