added config manager (closes #18) and fixed crash at win

This commit is contained in:
Nathan DECHER 2020-04-06 10:58:44 +02:00
parent a7e2d1c201
commit d339dd0a06
8 changed files with 331 additions and 120 deletions

View file

@ -5,7 +5,7 @@ FIRE_ANIM = $(foreach angle, $(shell seq 0 6 359), build/fire$(angle).png)
IMAGES = $(foreach name, apple wall, public/assets/$(name)32.png) IMAGES = $(foreach name, apple wall, public/assets/$(name)32.png)
TILESETS = $(foreach name, hole, public/assets/$(name)-ts.png) TILESETS = $(foreach name, hole, public/assets/$(name)-ts.png)
ANIMATIONS = $(foreach name, fire, public/assets/$(name)-anim.png) ANIMATIONS = $(foreach name, fire, public/assets/$(name)-anim.png)
JSON = $(foreach name, snake levelList config, public/assets/$(name).json) JSON = $(foreach name, snake levelList config metaConfig, public/assets/$(name).json)
ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico
CSS = public/css/snek.css CSS = public/css/snek.css
JS = public/js/snek.js JS = public/js/snek.js

View file

@ -1,20 +1,20 @@
{ {
"touchscreen": { "input.touchscreen.crosspad.enabled": true,
"enabled": true, "input.touchscreen.crosspad.overlay": true,
"mode": "crosspad",
"deadzone": 50, "input.touchscreen.joystick.enabled": false,
"buffer": false "input.touchscreen.joystick.overlay": true,
}, "input.touchscreen.joystick.deadzone": 10,
"keyboard": {
"enabled": true, "input.touchscreen.swipe.enabled": false,
"buffer": false "input.touchscreen.swipe.deadzone": 50,
},
"gamepad": { "input.gamepad.enabled": true,
"enabled": true, "input.gamepad.deadzone": 0.5,
"deadzone": 0.5,
"buffer": true "input.keyboard.enabled": true,
},
"appearance": { "input.buffer": false,
"grid": "none"
} "appearance.grid": "none"
} }

View file

@ -1,26 +1,121 @@
{ {
"touchscreen": { "input": {
"mode": [ "name": "Input settings"
"crosspad",
"joystick",
"swipe"
],
"deadzone": {
"min": 1,
"max": 100
}
}, },
"gamepad": {
"deadzone": { "input.touchscreen": {
"min": 0, "name": "Touchscreen settings"
"max": 1
}
}, },
"appearance": {
"grid": [ "input.touchscreen.crosspad": {
"grid", "name": "Crosspad mode"
"checkerboard", },
"none" "input.touchscreen.crosspad.enabled": {
"name": "Enable crosspad",
"type": "boolean",
"excludes": [
"input.touchscreen.joystick.enabled",
"input.touchscreen.swipe.enabled"
] ]
},
"input.touchscreen.crosspad.overlay": {
"name": "Show overlay",
"type": "boolean",
"parent": "input.touchscreen.crosspad.enabled"
},
"input.touchscreen.joystick": {
"name": "Joystick mode"
},
"input.touchscreen.joystick.enabled": {
"name": "Enable joystick",
"type": "boolean",
"excludes": [
"input.touchscreen.crosspad.enabled",
"input.touchscreen.swipe.enabled"
]
},
"input.touchscreen.joystick.overlay": {
"name": "Show overlay",
"type": "boolean",
"parent": "input.touchscreen.joystick.enabled"
},
"input.touchscreen.joystick.deadzone": {
"name": "Deadzone",
"type": "number",
"parent": "input.touchscreen.joystick.enabled",
"bounds": {
"min": 1,
"max": 100,
"inc": 1
}
},
"input.touchscreen.swipe": {
"name": "Swipe mode"
},
"input.touchscreen.swipe.enabled": {
"name": "Enable swipe",
"type": "boolean",
"excludes": [
"input.touchscreen.crosspad.enabled",
"input.touchscreen.joystick.enabled"
]
},
"input.touchscreen.swipe.deadzone": {
"name": "Deadzone",
"type": "number",
"parent": "input.touchscreen.swipe.enabled",
"bounds": {
"min": 1,
"max": 100,
"inc": 1
}
},
"input.gamepad": {
"name": "Gamepad settings"
},
"input.gamepad.enabled": {
"name": "Enable gamepad",
"type": "boolean"
},
"input.gamepad.deadzone": {
"name": "Deadzone",
"type": "number",
"parent": "input.gamepad.enabled",
"bounds": {
"min": 0,
"max": 1,
"inc": 0.1
}
},
"input.keyboard": {
"name": "Keyboard settings"
},
"input.keyboard.enabled": {
"name": "Enable keyboard",
"type": "boolean"
},
"input.buffer": {
"name": "Enable input buffering",
"type": "boolean"
},
"appearance": {
"name": "Appearance"
},
"appearance.grid": {
"name": "Grid type",
"type": "choice",
"bounds": {
"choices": [
"none",
"grid",
"checkerboard"
]
}
} }
} }

View file

@ -1,13 +1,14 @@
const ProgressBar=require('progress'); const ProgressBar=require('progress');
const assetSpecs=[ const assetSpecs=[
{ name: 'fruit', filename: 'apple32.png', type: 'image' }, { name: 'fruit', filename: 'apple32.png', type: 'image' },
{ name: 'wall', filename: 'wall32.png', type: 'image' }, { name: 'wall', filename: 'wall32.png', type: 'image' },
{ name: 'hole', filename: 'hole-ts.png', type: 'image' }, { name: 'hole', filename: 'hole-ts.png', type: 'image' },
{ name: 'fire', filename: 'fire-anim.png', type: 'image' }, { name: 'fire', filename: 'fire-anim.png', type: 'image' },
{ name: 'snake', filename: 'snake.json', type: 'json' }, { name: 'snake', filename: 'snake.json', type: 'json' },
{ name: 'levelList', filename: 'levelList.json', type: 'json' }, { name: 'levelList', filename: 'levelList.json', type: 'json' },
{ name: 'config', filename: 'config.json', type: 'json' } { name: 'config', filename: 'config.json', type: 'json' },
{ name: 'metaConfig', filename: 'metaConfig.json', type: 'json' }
]; ];
const tasks=[ const tasks=[

76
src/js/config.js Normal file
View file

@ -0,0 +1,76 @@
const assets=require('assets');
let watchers=Object.create(null);
let lastWatchCode=1;
const toBoolean=v => {
if(v=='false' || v==false) return false;
return true;
};
const get=key => {
let confVal=localStorage.getItem('config.'+key);
if(confVal===null) return assets.get('config')[key];
return confVal;
};
const getB=key => toBoolean(get(key));
const getN=key => +get(key);
const getS=key => ''+get(key);
const set=(key, value) => {
localStorage.setItem('config.'+key, value);
let interested=watchers[key];
if(interested) interested.forEach(watcher => watcher(key, value));
};
const remove=key => {
localStorage.removeItem('config.'+key, value);
let interested=watchers[key];
if(interested) interested.forEach(watcher => watcher(key, assets.get('config')[key]));
};
const clear=() =>
Object
.keys(assets.get('config'))
.forEach(remove);
const watch=(key, fn) => {
if(!watchers[key]) watchers[key]=[];
const code='w'+lastWatchCode++;
watchers[key][code]=fn;
return code;
};
const watchB=(key, fn) => watch(key, (k, v) => fn(k, toBoolean(v)));
const watchN=(key, fn) => watch(key, (k, v) => fn(k, +v));
const watchS=(key, fn) => watch(key, (k, v) => fn(k, ''+v));
const unwatch=(key, code) => {
if(!watchers[key]) return;
delete watchers[key][code];
};
const list=() =>
Object
.keys(assets.get('config'));
const dict=() => {
let dict=Object.create(null);
Object
.keys(assets.get('config'))
.forEach(
key => dict[key]={
raw: get(key),
b: getB(key),
n: getN(key),
s: getS(key)
}
);
return dict;
};
return module.exports={
get, getB, getN, getS,
set,
remove, clear,
watch, watchB, watchN, watchS,
unwatch,
list, dict
};

View file

@ -1,6 +1,7 @@
const config=require('config');
let currentInputs={}; let currentInputs={};
let handlers=[]; let handlers=[];
let config;
let hud; let hud;
const toAngleMagnitude=(x, y) => { const toAngleMagnitude=(x, y) => {
@ -10,7 +11,7 @@ const toAngleMagnitude=(x, y) => {
}; };
}; };
const handleAngleMagnitude=(x, y, threshold=0, fn=null, clearBuffer=false) => { const handleAngleMagnitude=(x, y, threshold=0, fn=null) => {
const {angle, magnitude}=toAngleMagnitude(x, y); const {angle, magnitude}=toAngleMagnitude(x, y);
if(magnitude>threshold) { if(magnitude>threshold) {
@ -20,7 +21,6 @@ const handleAngleMagnitude=(x, y, threshold=0, fn=null, clearBuffer=false) => {
else if(angle>1.25 && angle<1.75) inputs.left=true; else if(angle>1.25 && angle<1.75) inputs.left=true;
else inputs.down=true; else inputs.down=true;
if(clearBuffer) inputs.clearBuffer=true;
if(fn) fn(angle, magnitude); if(fn) fn(angle, magnitude);
} }
} }
@ -46,19 +46,42 @@ const handleCrosspad=(() => {
dl.setAttribute('stroke', 'black'); dl.setAttribute('stroke', 'black');
cross.appendChild(dl); cross.appendChild(dl);
let useOverlay=false;
let enabled=false;
const displayOverlay=() => {
if(hud) {
if(useOverlay && enabled) hud.appendChild(cross);
else hud.removeChild(cross);
}
};
config.watchB('input.touchscreen.crosspad.overlay', (k, v) => {
useOverlay=v;
displayOverlay();
});
const fn=e => const fn=e =>
handleAngleMagnitude( handleAngleMagnitude(
e.touches[0].clientX-window.innerWidth/2, e.touches[0].clientX-window.innerWidth/2,
e.touches[0].clientY-window.innerHeight/2, e.touches[0].clientY-window.innerHeight/2,
0, 0,
null, null
!config.touchscreen.buffer
); );
const init=() => {
useOverlay=config.getB('input.touchscreen.crosspad.overlay');
enabled=true;
displayOverlay();
};
const fini=() => {
enabled=false;
displayOverlay();
};
return { return {
touchstart: fn, touchstart: fn,
touchmove: fn, touchmove: fn,
init: () => hud.appendChild(cross), init,
fini: () => hud.removeChild(cross) fini
}; };
})(); })();
@ -69,8 +92,6 @@ const handleKeyboard={
else if(e.key=='ArrowDown') inputs.down=true; else if(e.key=='ArrowDown') inputs.down=true;
else if(e.key=='ArrowLeft') inputs.left=true; else if(e.key=='ArrowLeft') inputs.left=true;
else if(e.key=='ArrowRight') inputs.right=true; else if(e.key=='ArrowRight') inputs.right=true;
if(!config.keyboard.buffer) inputs.clearBuffer=true;
} }
}; };
@ -79,7 +100,17 @@ const handleJoystick=(() => {
x: 0, x: 0,
y: 0 y: 0
}; };
let deadzone;
const init=() => {
deadzone=config.getN('input.touchscreen.joystick.deadzone');
};
config.watchN('input.touchscreen.joystick.deadzone', (k, v) => {
deadzone=v;
});
return { return {
init,
touchstart: e => { touchstart: e => {
center.x=e.touches[0].clientX; center.x=e.touches[0].clientX;
center.y=e.touches[0].clientY; center.y=e.touches[0].clientY;
@ -88,9 +119,8 @@ const handleJoystick=(() => {
handleAngleMagnitude( handleAngleMagnitude(
e.touches[0].clientX-center.x, e.touches[0].clientX-center.x,
e.touches[0].clientY-center.y, e.touches[0].clientY-center.y,
config.touchscreen.deadzone, deadzone,
null, null
!config.touchscreen.buffer
) )
} }
})(); })();
@ -100,38 +130,59 @@ const handleSwipe=(() => {
x: 0, x: 0,
y: 0 y: 0
}; };
let deadzone;
let resetCenter=e => { let resetCenter=e => {
center.x=e.touches[0].clientX; center.x=e.touches[0].clientX;
center.y=e.touches[0].clientY; center.y=e.touches[0].clientY;
}; };
const init=() => {
deadzone=config.getN('input.touchscreen.swipe.deadzone');
};
config.watchN('input.touchscreen.swipe.deadzone', (k, v) => {
deadzone=v;
});
return { return {
init,
touchstart: resetCenter, touchstart: resetCenter,
touchmove: e => touchmove: e =>
handleAngleMagnitude( handleAngleMagnitude(
e.touches[0].clientX-center.x, e.touches[0].clientX-center.x,
e.touches[0].clientY-center.y, e.touches[0].clientY-center.y,
config.touchscreen.deadzone, deadzone,
() => resetCenter(e), () => resetCenter(e)
!config.touchscreen.buffer
) )
} }
})(); })();
const handleGamepads={ const handleGamepads=(() => {
frame: () => { let deadzone;
const gp=navigator.getGamepads()[0];
let inputs=currentInputs;
if(!gp || !gp.axes) return;
handleAngleMagnitude( const init=() => {
gp.axes[0], deadzone=config.getN('input.touchscreen.swipe.deadzone');
gp.axes[1], };
config.gamepad.deadzone, config.watchN('input.touchscreen.swipe.deadzone', (k, v) => {
null, deadzone=v;
!config.gamepad.buffer });
);
} return {
}; init,
frame: () => {
const gp=navigator.getGamepads()[0];
let inputs=currentInputs;
if(!gp || !gp.axes) return;
handleAngleMagnitude(
gp.axes[0],
gp.axes[1],
deadzone,
null
);
}
};
})();
const handleEvent=(type, evt) => { const handleEvent=(type, evt) => {
for(let handler of handlers) { for(let handler of handlers) {
@ -153,11 +204,22 @@ const disableHandler=handler => {
if(handler.fini) handler.fini(); if(handler.fini) handler.fini();
} }
}; };
const linkHandler=(handler, key) => {
if(config.getB(key)) enableHandler(handler);
config.watchB(key, (k, v) => {
if(v) enableHandler(handler);
else disableHandler(handler);
});
};
const updateConfig=cfg => const init=({hud: hudElem}) => {
config=cfg; hud=hudElem;
const setHud=elem => linkHandler(handleCrosspad, 'input.touchscreen.crosspad.enabled');
hud=elem; linkHandler(handleJoystick, 'input.touchscreen.joystick.enabled');
linkHandler(handleSwipe, 'input.touchscreen.swipe.enabled');
linkHandler(handleGamepads, 'input.gamepad.enabled');
linkHandler(handleKeyboard, 'input.keyboard.enabled');
};
const clear=() => const clear=() =>
Object Object
@ -171,14 +233,6 @@ for(let type of ['keydown', 'touchstart', 'touchmove']) {
return module.exports={ return module.exports={
inputs: currentInputs, inputs: currentInputs,
clear, clear,
enableHandler, disableHandler,
framefn: handleEvent.bind(null, 'frame'), framefn: handleEvent.bind(null, 'frame'),
availableHandlers: { init
keyboard: handleKeyboard,
gamepad: handleGamepads,
touchscreenCrosspad: handleCrosspad,
touchscreenJoystick: handleJoystick,
touchscreenSwipe: handleSwipe
},
updateConfig, setHud
}; };

View file

@ -6,6 +6,7 @@
const SnekGame=require('snek'); const SnekGame=require('snek');
const input=require('input'); const input=require('input');
const levels=require('levels'); const levels=require('levels');
const config=require('config');
// get a known state // get a known state
await new Promise(ok => assets.onReady(ok)); await new Promise(ok => assets.onReady(ok));
@ -17,9 +18,6 @@
const canvas=main.querySelector('canvas'); const canvas=main.querySelector('canvas');
const hud=main.querySelector('#hud'); const hud=main.querySelector('#hud');
// load config
const config=assets.get('config'); //TODO use an actual config module
// load data from server // load data from server
const levelList=assets.get('levelList'); const levelList=assets.get('levelList');
@ -115,7 +113,6 @@
handleDeath(snek); handleDeath(snek);
} }
}; };
snek.config=config;
// setup the DOM // setup the DOM
nav.classList.add('hidden'); nav.classList.add('hidden');
@ -152,6 +149,9 @@
// hide the HUD // hide the HUD
hud.classList.add('hidden'); hud.classList.add('hidden');
// fetch userdata from the game
const {category, levelId, filename}=snek.userdata;
// create and configure popup // create and configure popup
let popup=new Popup("Finished!"); let popup=new Popup("Finished!");
popup.addStrong("You won!"); popup.addStrong("You won!");
@ -174,7 +174,6 @@
// act on it // act on it
if(result=='retry') { if(result=='retry') {
const {category, levelId, filename}=snek.userdata;
startGame(category, levelId, filename); startGame(category, levelId, filename);
} else if(result=='menu') { } else if(result=='menu') {
location.hash='menu'; location.hash='menu';
@ -244,22 +243,6 @@
startGame(category, levelId, filename); startGame(category, levelId, filename);
}); });
// enable input methods according to config // enable input methods overlay
input.setHud(hud); input.init({hud});
if(config.keyboard.enabled) {
input.enableHandler(input.availableHandlers.keyboard);
}
if(config.gamepad.enabled) {
input.enableHandler(input.availableHandlers.gamepad);
}
if(config.touchscreen.enabled) {
if(config.touchscreen.mode=='crosspad') {
input.enableHandler(input.availableHandlers.touchscreenCrosspad);
} else if(config.touchscreen.mode=='joystick') {
input.enableHandler(input.availableHandlers.touchscreenJoystick);
} else if(config.touchscreen.mode=='swipe') {
input.enableHandler(input.availableHandlers.touchscreenSwipe);
}
}
input.updateConfig(config);
})(); })();

View file

@ -104,7 +104,7 @@ class SnekGame {
draw() { draw() {
const assets=require('assets'); const assets=require('assets');
const config=this.config; const config=require('config');
// clear the canvas, because it's easier than having to deal with everything // clear the canvas, because it's easier than having to deal with everything
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
@ -125,7 +125,7 @@ class SnekGame {
this.ctx.fillRect(0, offsetY+cellSize*this.dimensions[1], this.canvas.width, offsetY); this.ctx.fillRect(0, offsetY+cellSize*this.dimensions[1], this.canvas.width, offsetY);
// draw a grid/checkerboard if requested // draw a grid/checkerboard if requested
if(config.appearance.grid=='grid') { if(config.get('appearance.grid')=='grid') {
this.ctx.strokeStyle='rgba(0, 0, 0, 50%)'; this.ctx.strokeStyle='rgba(0, 0, 0, 50%)';
this.ctx.lineCap='square'; this.ctx.lineCap='square';
this.ctx.lineWidth=1; this.ctx.lineWidth=1;
@ -139,7 +139,7 @@ class SnekGame {
this.ctx.lineTo(this.canvas.width-offsetX, offsetY+y*cellSize); this.ctx.lineTo(this.canvas.width-offsetX, offsetY+y*cellSize);
} }
this.ctx.stroke(); this.ctx.stroke();
} else if(config.appearance.grid=='checkerboard') { } else if(config.get('appearance.grid')=='checkerboard') {
this.ctx.fillStyle='rgba(0, 0, 0, 10%)'; this.ctx.fillStyle='rgba(0, 0, 0, 10%)';
for(let x=0; x<this.dimensions[0]; x++) { for(let x=0; x<this.dimensions[0]; x++) {
for(let y=(x+1)%2; y<this.dimensions[1]; y+=2) { for(let y=(x+1)%2; y<this.dimensions[1]; y+=2) {
@ -400,6 +400,8 @@ class SnekGame {
} }
handleInputs(inputs) { handleInputs(inputs) {
const config=require('config');
// change direction if the input is valid // change direction if the input is valid
const trySet=(dir) => { const trySet=(dir) => {
if(!dir.every((e, i) => e==this.lastDirection[i] || e==-this.lastDirection[i])) { if(!dir.every((e, i) => e==this.lastDirection[i] || e==-this.lastDirection[i])) {
@ -420,13 +422,13 @@ class SnekGame {
}); });
// try all inputs in order and unbuffer them if valid // try all inputs in order and unbuffer them if valid
if(inputs.left && trySet([-1, 0])) return delete inputs.left; if(inputs.left && trySet([-1, 0])) delete inputs.left;
else if(inputs.right && trySet([ 1, 0])) return delete inputs.right; else if(inputs.right && trySet([ 1, 0])) delete inputs.right;
else if(inputs.up && trySet([ 0,-1])) return delete inputs.up; else if(inputs.up && trySet([ 0,-1])) delete inputs.up;
else if(inputs.down && trySet([ 0, 1])) return delete inputs.down; else if(inputs.down && trySet([ 0, 1])) delete inputs.down;
// buffering might be disabled // buffering might be disabled
if(inputs.clearBuffer) { if(!config.getB('input.buffer')) {
Object Object
.keys(inputs) .keys(inputs)
.forEach(k => delete inputs[k]); .forEach(k => delete inputs[k]);