diff options
Diffstat (limited to 'src/_posts/2021-04-11-ripple-v2.md')
-rw-r--r-- | src/_posts/2021-04-11-ripple-v2.md | 436 |
1 files changed, 0 insertions, 436 deletions
diff --git a/src/_posts/2021-04-11-ripple-v2.md b/src/_posts/2021-04-11-ripple-v2.md deleted file mode 100644 index cbde032..0000000 --- a/src/_posts/2021-04-11-ripple-v2.md +++ /dev/null @@ -1,436 +0,0 @@ ---- -title: >- - Ripple V2: A Better Game -description: >- - The sequel no one was waiting for! -tags: tech -series: ripple ---- - -<p> - <b>Movement:</b> Arrow keys or WASD<br/> - <b>Jump:</b> Space<br/> - <b>Goal:</b> Jump as many times as possible without touching a ripple!<br/> - <br/> - <b>Press Jump To Begin!</b> -</p> - -_Who can make the muddy water clear?<br/> -Let it be still, and it will gradually become clear._ - -<canvas id="canvas" - style="border:1px dashed #AAA" - tabindex=0> -Your browser doesn't support canvas. At this point in the world that's actually -pretty cool, well done! -</canvas> -<button onclick="reset()">(R)eset</button> -<span style="font-size: 2rem; margin-left: 1rem;">Score: - <span style="font-weight: bold" id="score">0</span> -</span> - -<script type="text/javascript"> - -const palette = [ - "#264653", - "#2A9D8F", - "#E9C46A", - "#F4A261", - "#E76F51", -]; - -const width = 800; -const height = 600; - -function hypotenuse(w, h) { - return Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2)); -} - -let canvas = document.getElementById("canvas"); -canvas.width = width; -canvas.height = height; - -const whitelistedKeys = { - "ArrowUp": {}, - "KeyW": {map: "ArrowUp"}, - "ArrowLeft": {}, - "KeyA": {map: "ArrowLeft"}, - "ArrowRight": {}, - "KeyD": {map: "ArrowRight"}, - "ArrowDown": {}, - "KeyS": {map: "ArrowDown"}, - "Space": {}, - "KeyR": {}, -}; - -let keyboard = {}; - -canvas.addEventListener('keydown', (event) => { - let keyInfo = whitelistedKeys[event.code]; - if (!keyInfo) return; - - let code = event.code; - if (keyInfo.map) code = keyInfo.map; - - event.preventDefault(); - keyboard[code] = true; -}); - -canvas.addEventListener('keyup', (event) => { - let keyInfo = whitelistedKeys[event.code]; - if (!keyInfo) return; - - let code = event.code; - if (keyInfo.map) code = keyInfo.map; - - event.preventDefault(); - delete keyboard[code]; -}); - - -const C = 700; // scales the overall speed of the radius -const T = 500; // on which tick the radius change becomes linear - -/* - f(x) = sqrt(C*x) when x < T - (C/(2*sqrt(CT)))(x-T) + sqrt(CT) when x >= T - - radius(x) = f(x) + playerRadius; -*/ - -const F1 = (x) => Math.sqrt(C*x); -const F2C1 = C / (2 * Math.sqrt(C*T)); -const F2C2 = Math.sqrt(C * T); -const F2 = (x) => (F2C1 * (x - T)) + F2C2; -const F = (x) => { - if (x < T) return F1(x); - return F2(x); -}; - -class Ripple { - - constructor(id, currTick, x, y, bounces, color) { - this.id = id; - this.tick = currTick; - this.x = x; - this.y = y; - this.thickness = Math.pow(bounces+1, 1.25); - this.color = color; - this.winner = false; - - this.maxRadius = hypotenuse(x, y); - this.maxRadius = Math.max(this.maxRadius, hypotenuse(width-x, y)); - this.maxRadius = Math.max(this.maxRadius, hypotenuse(x, height-y)); - this.maxRadius = Math.max(this.maxRadius, hypotenuse(width-x, height-y)); - } - - radius(currTick) { - const x = currTick - this.tick; - return F(x) + playerRadius; - } - - draw(ctx, currTick) { - ctx.beginPath(); - ctx.arc(this.x, this.y, this.radius(currTick), 0, Math.PI * 2, false); - ctx.closePath(); - ctx.lineWidth = this.thickness; - ctx.strokeStyle = this.winner ? "#FF0000" : this.color; - ctx.stroke(); - } - - canGC(currTick) { - return this.radius(currTick) > this.maxRadius; - } -} - -const playerRadius = 10; -const playerMoveAccel = 0.5; -const playerMoveDecel = 0.7; -const playerMaxMoveSpeed = 4; -const playerJumpSpeed = 0.08; -const playerMaxHeight = 1; -const playerGravity = 0.01; - -class Player{ - - constructor(x, y, color) { - this.x = x; - this.y = y; - this.z = 0; - this.xVelocity = 0; - this.yVelocity = 0; - this.zVelocity = 0; - this.color = color; - this.falling = false; - this.lastJumpHeight = 0; - this.loser = false; - } - - act() { - if (keyboard["ArrowUp"]) { - this.yVelocity = Math.max(-playerMaxMoveSpeed, this.yVelocity - playerMoveAccel); - } else if (keyboard["ArrowDown"]) { - this.yVelocity = Math.min(playerMaxMoveSpeed, this.yVelocity + playerMoveAccel); - } else if (this.yVelocity > 0) { - this.yVelocity = Math.max(0, this.yVelocity - playerMoveDecel); - } else if (this.yVelocity < 0) { - this.yVelocity = Math.min(0, this.yVelocity + playerMoveDecel); - } - - this.y += this.yVelocity; - this.y = Math.max(0+playerRadius, this.y); - this.y = Math.min(height-playerRadius, this.y); - - if (keyboard["ArrowLeft"]) { - this.xVelocity = Math.max(-playerMaxMoveSpeed, this.xVelocity - playerMoveAccel); - } else if (keyboard["ArrowRight"]) { - this.xVelocity = Math.min(playerMaxMoveSpeed, this.xVelocity + playerMoveAccel); - } else if (this.xVelocity > 0) { - this.xVelocity = Math.max(0, this.xVelocity - playerMoveDecel); - } else if (this.xVelocity < 0) { - this.xVelocity = Math.min(0, this.xVelocity + playerMoveDecel); - } - - this.x += this.xVelocity; - this.x = Math.max(0+playerRadius, this.x); - this.x = Math.min(width-playerRadius, this.x); - - let jumpHeld = keyboard["Space"]; - - if (jumpHeld && !this.falling && this.z < playerMaxHeight) { - this.lastJumpHeight = 0; - this.zVelocity = playerJumpSpeed; - } else { - this.zVelocity = Math.max(-playerJumpSpeed, this.zVelocity - playerGravity); - this.falling = this.z > 0; - } - - let prevZ = this.z; - this.z = Math.max(0, this.z + this.zVelocity); - this.lastJumpHeight = Math.max(this.z, this.lastJumpHeight); - } - - draw(ctx) { - let y = this.y - (this.z * 40); - let radius = playerRadius * (this.z+1) - - // draw main - ctx.beginPath(); - ctx.arc(this.x, y, radius, 0, Math.PI * 2, false); - ctx.closePath(); - ctx.lineWidth = 0; - ctx.fillStyle = this.color; - ctx.fill(); - if (this.loser) { - ctx.strokeStyle = '#FF0000'; - ctx.lineWidth = 2; - ctx.stroke(); - } - - // draw shadow, if in the air - if (this.z > 0) { - let radius = Math.max(0, playerRadius * (1.2 - this.z)); - ctx.beginPath(); - ctx.arc(this.x, this.y, radius, 0, Math.PI * 2, false); - ctx.closePath(); - ctx.lineWidth = 0; - ctx.fillStyle = this.color+"33"; - ctx.fill(); - } - } -} - -class Game { - - constructor(canvas, scoreEl) { - this.currTick = 0; - this.player = new Player(width/2, height/2, palette[0]); - this.state = 'play'; - this.score = 0; - this.scoreEl = scoreEl; - this.canvas = canvas; - this.ctx = canvas.getContext("2d"); - this.ripples = []; - this.nextRippleID = 0; - } - - shouldReset() { - return keyboard['KeyR']; - } - - newRippleID() { - let id = this.nextRippleID; - this.nextRippleID++; - return id; - } - - // newRipple initializes and stores a new ripple at the given coordinates, as - // well as all sub-ripples which make up the initial ripple's reflections. - newRipple(x, y, bounces, color) { - color = color ? color : palette[Math.floor(Math.random() * palette.length)]; - - let ripplePos = []; - let nextRipples = []; - - let addRipple = (x, y) => { - for (let i in ripplePos) { - if (ripplePos[i][0] == x && ripplePos[i][1] == y) return; - } - - let ripple = new Ripple(this.newRippleID(), this.currTick, x, y, bounces, color); - nextRipples.push(ripple); - ripplePos.push([x, y]); - this.ripples.push(ripple); - }; - - // add initial ripple, after this we deal with the sub-ripples. - addRipple(x, y); - - while (bounces > 0) { - bounces--; - let prevRipples = nextRipples; - nextRipples = []; - - for (let i in prevRipples) { - let prevX = prevRipples[i].x; - let prevY = prevRipples[i].y; - addRipple(prevX, -prevY); - addRipple(-prevX, prevY); - addRipple((2*this.canvas.width)-prevX, prevY); - addRipple(prevX, (2*this.canvas.height)-prevY); - } - } - } - - // playerRipplesState returns a mapping of rippleID -> boolean, where each - // boolean indicates the ripple's relation to the player at the moment. true - // indicates the player is outside the ripple, false indicates the player is - // within the ripple. - playerRipplesState() { - let state = {}; - for (let i in this.ripples) { - let ripple = this.ripples[i]; - let rippleRadius = ripple.radius(this.currTick); - let hs = Math.pow(ripple.x-this.player.x, 2) + Math.pow(ripple.y-this.player.y, 2); - state[ripple.id] = hs > Math.pow(rippleRadius + playerRadius, 2); - } - return state; - } - - playerHasJumpedOverRipple(prev, curr) { - for (const rippleID in prev) { - if (!curr.hasOwnProperty(rippleID)) continue; - if (curr[rippleID] != prev[rippleID]) return true; - } - return false; - } - - update() { - if (this.state != 'play') return; - - let playerPrevZ = this.player.z; - this.player.act(); - - if (playerPrevZ == 0 && this.player.z > 0) { - // player has jumped - this.prevPlayerRipplesState = this.playerRipplesState(); - - } else if (playerPrevZ > 0 && this.player.z == 0) { - - // player has landed, don't produce a ripple unless there are no - // existing ripples or the player jumped over an existing one. - if ( - this.ripples.length == 0 || - this.playerHasJumpedOverRipple( - this.prevPlayerRipplesState, - this.playerRipplesState() - ) - ) { - let bounces = Math.floor((this.player.lastJumpHeight*1.8)+1); - console.log("spawning ripple with bounces:", bounces); - this.newRipple(this.player.x, this.player.y, bounces); - this.score += bounces; - } - } - - if (this.player.z == 0) { - for (let i in this.ripples) { - let ripple = this.ripples[i]; - let rippleRadius = ripple.radius(this.currTick); - if (rippleRadius < playerRadius * 1.5) continue; - let hs = Math.pow(ripple.x-this.player.x, 2) + Math.pow(ripple.y-this.player.y, 2); - if (hs > Math.pow(rippleRadius + playerRadius, 2)) { - continue; - } else if (hs <= Math.pow(rippleRadius - playerRadius, 2)) { - continue; - } else { - console.log("game over", ripple); - ripple.winner = true; - this.player.loser = true; - this.state = 'gameOver'; - // deliberately don't break here, in case multiple ripples hit - // the player on the same frame - } - } - } - - this.ripples = this.ripples.filter(ripple => !ripple.canGC(this.currTick)); - - this.currTick++; - } - - draw() { - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ripples.forEach(ripple => ripple.draw(this.ctx, this.currTick)); - this.player.draw(this.ctx) - this.scoreEl.innerHTML = this.score; - } -} - - -const requestAnimationFrame = - window.requestAnimationFrame || - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || - window.msRequestAnimationFrame; - -let game = new Game(canvas, document.getElementById("score")); - -function reset() { - game = new Game(canvas, document.getElementById("score")); -} - -function nextFrame() { - if (game.shouldReset()) reset(); - - game.update() - game.draw() - requestAnimationFrame(nextFrame); -} -requestAnimationFrame(nextFrame); - -canvas.focus(); - -</script> - -## Changelog - -There's been two major changes to the mechanics of the game since the previous -version: - -* A new ripple is created _only_ if there are no ripples on the field already, - or if the player has jumped over an existing ripple. - -* The score is increased only if a ripple is created, and is increased by the - number of bounces off the wall that ripple will have. Put another way, the - score is increased based on how high you jump. - -Other small changes include: - -* Ripple growth rate has been modified. It's now harder for a player to run into - the ripple they just created. - -* Ripple thickness indicates how many bounces are left in the ripple. This was - the case previously, but it's been made more obvious. - -* Small performance improvements. |