From d339dd0a06107e0cc2898e02df4bf8211113404b Mon Sep 17 00:00:00 2001 From: Nathan DECHER Date: Mon, 6 Apr 2020 10:58:44 +0200 Subject: [PATCH] added config manager (closes #18) and fixed crash at win --- Makefile | 2 +- assets/config.json | 36 +++++------ assets/metaConfig.json | 127 ++++++++++++++++++++++++++++++++----- src/js/assets.js | 17 ++--- src/js/config.js | 76 +++++++++++++++++++++++ src/js/input.js | 138 ++++++++++++++++++++++++++++------------- src/js/main.js | 29 ++------- src/js/snek.js | 18 +++--- 8 files changed, 327 insertions(+), 116 deletions(-) create mode 100644 src/js/config.js diff --git a/Makefile b/Makefile index e1ede72..6a8c669 100644 --- a/Makefile +++ b/Makefile @@ -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) TILESETS = $(foreach name, hole, public/assets/$(name)-ts.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 CSS = public/css/snek.css JS = public/js/snek.js diff --git a/assets/config.json b/assets/config.json index a880e01..58ce63d 100644 --- a/assets/config.json +++ b/assets/config.json @@ -1,20 +1,20 @@ { - "touchscreen": { - "enabled": true, - "mode": "crosspad", - "deadzone": 50, - "buffer": false - }, - "keyboard": { - "enabled": true, - "buffer": false - }, - "gamepad": { - "enabled": true, - "deadzone": 0.5, - "buffer": true - }, - "appearance": { - "grid": "none" - } + "input.touchscreen.crosspad.enabled": true, + "input.touchscreen.crosspad.overlay": true, + + "input.touchscreen.joystick.enabled": false, + "input.touchscreen.joystick.overlay": true, + "input.touchscreen.joystick.deadzone": 10, + + "input.touchscreen.swipe.enabled": false, + "input.touchscreen.swipe.deadzone": 50, + + "input.gamepad.enabled": true, + "input.gamepad.deadzone": 0.5, + + "input.keyboard.enabled": true, + + "input.buffer": false, + + "appearance.grid": "none" } diff --git a/assets/metaConfig.json b/assets/metaConfig.json index 687d1de..38b58de 100644 --- a/assets/metaConfig.json +++ b/assets/metaConfig.json @@ -1,26 +1,121 @@ { - "touchscreen": { - "mode": [ - "crosspad", - "joystick", - "swipe" - ], - "deadzone": { + "input": { + "name": "Input settings" + }, + + "input.touchscreen": { + "name": "Touchscreen settings" + }, + + "input.touchscreen.crosspad": { + "name": "Crosspad mode" + }, + "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 + "max": 100, + "inc": 1 } }, - "gamepad": { - "deadzone": { + + "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 + "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": { - "grid": [ - "grid", - "checkerboard", - "none" - ] + "name": "Appearance" + }, + "appearance.grid": { + "name": "Grid type", + "type": "choice", + "bounds": { + "choices": [ + "none", + "grid", + "checkerboard" + ] + } } } diff --git a/src/js/assets.js b/src/js/assets.js index 0abe7f6..3c06c5d 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -1,13 +1,14 @@ const ProgressBar=require('progress'); const assetSpecs=[ - { name: 'fruit', filename: 'apple32.png', type: 'image' }, - { name: 'wall', filename: 'wall32.png', type: 'image' }, - { name: 'hole', filename: 'hole-ts.png', type: 'image' }, - { name: 'fire', filename: 'fire-anim.png', type: 'image' }, - { name: 'snake', filename: 'snake.json', type: 'json' }, - { name: 'levelList', filename: 'levelList.json', type: 'json' }, - { name: 'config', filename: 'config.json', type: 'json' } + { name: 'fruit', filename: 'apple32.png', type: 'image' }, + { name: 'wall', filename: 'wall32.png', type: 'image' }, + { name: 'hole', filename: 'hole-ts.png', type: 'image' }, + { name: 'fire', filename: 'fire-anim.png', type: 'image' }, + { name: 'snake', filename: 'snake.json', type: 'json' }, + { name: 'levelList', filename: 'levelList.json', type: 'json' }, + { name: 'config', filename: 'config.json', type: 'json' }, + { name: 'metaConfig', filename: 'metaConfig.json', type: 'json' } ]; const tasks=[ @@ -84,7 +85,7 @@ let readyListeners=[]; } break; } - + case 'animation': { let anim=assets[task.from]=[]; let frameCount=source.height/source.width; diff --git a/src/js/config.js b/src/js/config.js new file mode 100644 index 0000000..906aa26 --- /dev/null +++ b/src/js/config.js @@ -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 +}; diff --git a/src/js/input.js b/src/js/input.js index 84f4e32..ed280f4 100644 --- a/src/js/input.js +++ b/src/js/input.js @@ -1,6 +1,7 @@ +const config=require('config'); + let currentInputs={}; let handlers=[]; -let config; let hud; 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); 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 inputs.down=true; - if(clearBuffer) inputs.clearBuffer=true; if(fn) fn(angle, magnitude); } } @@ -46,19 +46,42 @@ const handleCrosspad=(() => { dl.setAttribute('stroke', 'black'); 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 => handleAngleMagnitude( e.touches[0].clientX-window.innerWidth/2, e.touches[0].clientY-window.innerHeight/2, 0, - null, - !config.touchscreen.buffer + null ); + + const init=() => { + useOverlay=config.getB('input.touchscreen.crosspad.overlay'); + enabled=true; + displayOverlay(); + }; + const fini=() => { + enabled=false; + displayOverlay(); + }; + return { touchstart: fn, touchmove: fn, - init: () => hud.appendChild(cross), - fini: () => hud.removeChild(cross) + init, + fini }; })(); @@ -69,8 +92,6 @@ const handleKeyboard={ else if(e.key=='ArrowDown') inputs.down=true; else if(e.key=='ArrowLeft') inputs.left=true; else if(e.key=='ArrowRight') inputs.right=true; - - if(!config.keyboard.buffer) inputs.clearBuffer=true; } }; @@ -79,7 +100,17 @@ const handleJoystick=(() => { x: 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 { + init, touchstart: e => { center.x=e.touches[0].clientX; center.y=e.touches[0].clientY; @@ -88,9 +119,8 @@ const handleJoystick=(() => { handleAngleMagnitude( e.touches[0].clientX-center.x, e.touches[0].clientY-center.y, - config.touchscreen.deadzone, - null, - !config.touchscreen.buffer + deadzone, + null ) } })(); @@ -100,38 +130,59 @@ const handleSwipe=(() => { x: 0, y: 0 }; + let deadzone; + let resetCenter=e => { center.x=e.touches[0].clientX; 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 { + init, touchstart: resetCenter, touchmove: e => handleAngleMagnitude( e.touches[0].clientX-center.x, e.touches[0].clientY-center.y, - config.touchscreen.deadzone, - () => resetCenter(e), - !config.touchscreen.buffer + deadzone, + () => resetCenter(e) ) } })(); -const handleGamepads={ - frame: () => { - const gp=navigator.getGamepads()[0]; - let inputs=currentInputs; - if(!gp || !gp.axes) return; +const handleGamepads=(() => { + let deadzone; - handleAngleMagnitude( - gp.axes[0], - gp.axes[1], - config.gamepad.deadzone, - null, - !config.gamepad.buffer - ); - } -}; + const init=() => { + deadzone=config.getN('input.touchscreen.swipe.deadzone'); + }; + config.watchN('input.touchscreen.swipe.deadzone', (k, v) => { + deadzone=v; + }); + + 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) => { for(let handler of handlers) { @@ -153,11 +204,22 @@ const disableHandler=handler => { 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 => - config=cfg; -const setHud=elem => - hud=elem; +const init=({hud: hudElem}) => { + hud=hudElem; + linkHandler(handleCrosspad, 'input.touchscreen.crosspad.enabled'); + linkHandler(handleJoystick, 'input.touchscreen.joystick.enabled'); + linkHandler(handleSwipe, 'input.touchscreen.swipe.enabled'); + linkHandler(handleGamepads, 'input.gamepad.enabled'); + linkHandler(handleKeyboard, 'input.keyboard.enabled'); +}; const clear=() => Object @@ -171,14 +233,6 @@ for(let type of ['keydown', 'touchstart', 'touchmove']) { return module.exports={ inputs: currentInputs, clear, - enableHandler, disableHandler, framefn: handleEvent.bind(null, 'frame'), - availableHandlers: { - keyboard: handleKeyboard, - gamepad: handleGamepads, - touchscreenCrosspad: handleCrosspad, - touchscreenJoystick: handleJoystick, - touchscreenSwipe: handleSwipe - }, - updateConfig, setHud + init }; diff --git a/src/js/main.js b/src/js/main.js index 7c0eb73..586ae53 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -6,6 +6,7 @@ const SnekGame=require('snek'); const input=require('input'); const levels=require('levels'); + const config=require('config'); // get a known state await new Promise(ok => assets.onReady(ok)); @@ -17,9 +18,6 @@ const canvas=main.querySelector('canvas'); const hud=main.querySelector('#hud'); - // load config - const config=assets.get('config'); //TODO use an actual config module - // load data from server const levelList=assets.get('levelList'); @@ -115,7 +113,6 @@ handleDeath(snek); } }; - snek.config=config; // setup the DOM nav.classList.add('hidden'); @@ -152,6 +149,9 @@ // hide the HUD hud.classList.add('hidden'); + // fetch userdata from the game + const {category, levelId, filename}=snek.userdata; + // create and configure popup let popup=new Popup("Finished!"); popup.addStrong("You won!"); @@ -174,7 +174,6 @@ // act on it if(result=='retry') { - const {category, levelId, filename}=snek.userdata; startGame(category, levelId, filename); } else if(result=='menu') { location.hash='menu'; @@ -244,22 +243,6 @@ startGame(category, levelId, filename); }); - // enable input methods according to config - input.setHud(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); + // enable input methods overlay + input.init({hud}); })(); diff --git a/src/js/snek.js b/src/js/snek.js index 586d028..4fd8d70 100644 --- a/src/js/snek.js +++ b/src/js/snek.js @@ -104,7 +104,7 @@ class SnekGame { draw() { 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 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); // 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.lineCap='square'; this.ctx.lineWidth=1; @@ -139,7 +139,7 @@ class SnekGame { this.ctx.lineTo(this.canvas.width-offsetX, offsetY+y*cellSize); } this.ctx.stroke(); - } else if(config.appearance.grid=='checkerboard') { + } else if(config.get('appearance.grid')=='checkerboard') { this.ctx.fillStyle='rgba(0, 0, 0, 10%)'; for(let x=0; x { 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 - if(inputs.left && trySet([-1, 0])) return delete inputs.left; - else if(inputs.right && trySet([ 1, 0])) return delete inputs.right; - else if(inputs.up && trySet([ 0,-1])) return delete inputs.up; - else if(inputs.down && trySet([ 0, 1])) return delete inputs.down; + if(inputs.left && trySet([-1, 0])) delete inputs.left; + else if(inputs.right && trySet([ 1, 0])) delete inputs.right; + else if(inputs.up && trySet([ 0,-1])) delete inputs.up; + else if(inputs.down && trySet([ 0, 1])) delete inputs.down; // buffering might be disabled - if(inputs.clearBuffer) { + if(!config.getB('input.buffer')) { Object .keys(inputs) .forEach(k => delete inputs[k]);