diff --git a/Makefile b/Makefile index 341a0a3..392dad0 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,21 @@ .PHONY: all clean +IMAGES = $(foreach name, apple wall, public/assets/$(name)32.png) +TILESETS = $(foreach name, hole, public/assets/$(name)-ts.png) +JSON = $(foreach name, snake levelList config, public/assets/$(name).json) ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico -APPLE = public/assets/apple32.png -WALL = public/assets/wall32.png - -SNAKE = public/assets/snake.json -LEVEL_LIST = public/assets/levelList.json -CONFIG = public/assets/config.json - CSS = public/css/snek.css - JS = public/js/snek.js -OUTPUT = $(ICON) $(APPLE) $(WALL) $(SNAKE) $(LEVEL_LIST) $(CONFIG) $(CSS) $(JS) +OUTPUT = $(IMAGES) $(TILESETS) $(JSON) $(ICON) $(CSS) $(JS) -all: icon apple wall snake levelList config css js -icon: $(ICON) -apple: $(APPLE) -wall: $(WALL) - -snake: $(SNAKE) -levelList: $(LEVEL_LIST) -config: $(CONFIG) +all: images tilesets json icon css js +images: $(IMAGES) +tilesets: $(TILESETS) +json: $(JSON) +icon: $(ICON) css: $(CSS) - js: $(JS) public/favicon.ico: assets/icon.jpg @@ -40,6 +31,9 @@ public/assets/%32.png: assets/%.jpg public/assets/%256.png: assets/%.jpg convert $^ -resize 256x $@ +public/assets/%-ts.png: assets/%.png + convert $^ -scale 32x $@ + public/assets/%.json: assets/%.json cp $^ $@ diff --git a/assets/hole.png b/assets/hole.png new file mode 100755 index 0000000..a8278e0 Binary files /dev/null and b/assets/hole.png differ diff --git a/assets/hole.xcf b/assets/hole.xcf new file mode 100755 index 0000000..17e3b02 Binary files /dev/null and b/assets/hole.xcf differ diff --git a/src/js/assets.js b/src/js/assets.js index 16d15e7..8a9dc67 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -3,18 +3,23 @@ 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: 'snake', filename: 'snake.json', type: 'json' }, { name: 'levelList', filename: 'levelList.json', type: 'json' }, { name: 'config', filename: 'config.json', type: 'json' } ]; +const tasks=[ + { from: 'hole', type: 'tileset', tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'], steps: 9 } +]; + const cvs=document.createElement('canvas'); cvs.width=400; cvs.height=50; cvs.classList.add('progressBar'); cvs.classList.add('hiddenBottom'); -const bar=new ProgressBar(assetSpecs.length*2); +const bar=new ProgressBar(assetSpecs.length*2+tasks.reduce((a, t) => a+t.steps, 0)); bar.addUpdateListener(() => bar.draw(cvs)); bar.draw(cvs); @@ -27,11 +32,11 @@ bar.addReadyListener(() => { }); //XXX purposefully slow down asset loading -const sleep=(ms) => new Promise(ok => setTimeout(ok, ms)); +const sleep=(ms=1000) => new Promise(ok => setTimeout(ok, ms*Math.random())); const loadAsset=async (asset) => { const response=await fetch('assets/'+asset.filename); - await sleep(1000*Math.random()); + await sleep(); bar.update(); let result; switch(asset.type) { @@ -43,7 +48,7 @@ const loadAsset=async (asset) => { result=await createImageBitmap(await response.blob()); break; } - await sleep(1000*Math.random()); + await sleep(); bar.update(); return [asset.name, result]; }; @@ -52,18 +57,37 @@ let assets=Object.create(null); let ready=false; let readyListeners=[]; -Promise - .all( - assetSpecs.map(a => loadAsset(a)) - ).then(arr => { - arr.forEach(([name, value]) => { - assets[name]=value; - }); - ready=true; - readyListeners.forEach(fn => fn.bind(fn)()); - readyListeners=null; +(async () => { + let arr=await Promise + .all( + assetSpecs.map(a => loadAsset(a)) + ); + + arr.forEach(([name, value]) => { + assets[name]=value; }); + for(let task of tasks) { + const source=assets[task.from]; + switch(task.type) { + case 'tileset': { + let asset=assets[task.from]=Object.create(null); + for(let tId in task.tiles) { + const tName=task.tiles[tId]; + asset[tName]=await createImageBitmap(source, 0, source.width*tId, source.width, source.width); + await sleep(100); + bar.update(); + } + break; + } + } + } + + ready=true; + readyListeners.forEach(fn => fn.bind(fn)()); + readyListeners=null; +})(); + const onReady=(fn) => { if(ready) fn.bind(fn)(); else readyListeners.push(fn); diff --git a/src/js/snek.js b/src/js/snek.js index 271a33e..fde0d3c 100644 --- a/src/js/snek.js +++ b/src/js/snek.js @@ -1,6 +1,4 @@ -const [EMPTY, FOOD, WALL, SNAKE]=Array(4).keys(); - -const ifNaN=(v, r) => isNaN(v)?r:v; +const [EMPTY, FOOD, WALL, HOLE, HOLE_S, SNAKE]=Array(6).keys(); class SnekGame { constructor(settings, canvas, rules) { @@ -20,11 +18,12 @@ class SnekGame { case ' ': return EMPTY; case 'f': return FOOD; case 'w': return WALL; + case 'o': return HOLE; } })(); } } - // + // extract the dimensions this.dimensions=[this.world.length, this.world[0].length]; @@ -51,6 +50,9 @@ class SnekGame { // add the walls if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=WALL); + // add the holes + if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=HOLE); + // add the food settings.food.forEach(([x, y]) => this.world[x][y]=FOOD); this.fruits=[...settings.food]; @@ -62,9 +64,13 @@ class SnekGame { // get the head and initial direction this.head=[...settings.snake[0]]; - this.direction=[ - ifNaN(settings.snake[0][0]-settings.snake[1][0], 1), - ifNaN(settings.snake[0][1]-settings.snake[1][1], 0) + if(settings.snake.length>=2) this.direction=[ + settings.snake[0][0]-settings.snake[1][0], + settings.snake[0][1]-settings.snake[1][1] + ]; + else this.direction=[ + 1, + 0 ]; this.lastDirection=this.direction @@ -115,18 +121,46 @@ class SnekGame { // draw our walls const wall=assets.get('wall'); + const hole=assets.get('hole'); + + const putTile=(x, y, tile) => this.ctx.drawImage( + tile, + offsetX+cellSize*x, + offsetY+cellSize*y, + cellSize, + cellSize + ); + const checkAdj=(x, y) => { + let adj={}; + adj.u=this.world[x][y-1]; + adj.d=this.world[x][y+1]; + adj.l=(this.world[x-1] || [])[y]; + adj.r=(this.world[x+1] || [])[y]; + adj.ul=(this.world[x-1] || [])[y-1]; + adj.ur=(this.world[x+1] || [])[y-1]; + adj.dl=(this.world[x-1] || [])[y+1]; + adj.dr=(this.world[x+1] || [])[y+1]; + return adj; + }; for(let x=0; x adj[k]==HOLE || adj[k]==HOLE_S) + .forEach(k => putTile(x, y, hole[k])); break; + // technically, this works for all shapes + // however, the tileset only handles convex shapes + } } } } @@ -198,7 +232,13 @@ class SnekGame { // get our tail out of the way const tail=this.snake.pop(); - this.world[tail[0]][tail[1]]=EMPTY; + switch(this.world[tail[0]][tail[1]]) { + case HOLE_S: + this.world[tail[0]][tail[1]]=HOLE; + break; + default: + this.world[tail[0]][tail[1]]=EMPTY; + } // check for out of world conditions if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) { @@ -214,8 +254,21 @@ class SnekGame { // you hit, you die case WALL: case SNAKE: + case HOLE_S: return this.die(); + // if either 3 consecutive segments or the whole snake is on a hole, you die + case HOLE: + if( + this.snake.length==0 || + this.snake.length==1 && + this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S || + this.snake.length>=2 && + this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S && + this.world[this.snake[1][0]][this.snake[1][1]]==HOLE_S + ) return this.die(); + break; + // you eat, you don't die case FOOD: // re-grow the snake @@ -251,7 +304,13 @@ class SnekGame { } // move our head forward - this.world[head[0]][head[1]]=SNAKE; + switch(this.world[head[0]][head[1]]) { + case HOLE: + this.world[head[0]][head[1]]=HOLE_S; + break; + default: + this.world[head[0]][head[1]]=SNAKE; + } this.snake.unshift(head); // automatic speed increase @@ -301,6 +360,7 @@ class SnekGame { } handleInputs(inputs) { + // change direction if the input is valid const trySet=(dir) => { if(!dir.every((e, i) => e==this.lastDirection[i] || e==-this.lastDirection[i])) { this.direction=dir; @@ -308,6 +368,7 @@ class SnekGame { } } + // reduce buffer duration Object .keys(inputs) .forEach(k => { @@ -318,11 +379,13 @@ class SnekGame { else inputs[k]=v; }); + // 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; + // buffering might be disabled if(inputs.clearBuffer) { Object .keys(inputs)