diff --git a/TODO.md b/TODO.md index 113ca53..ba2a167 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,14 @@ Stuff +- mobile exploration: + + - gameplay can actually be ok on iOS? + - canvas fill fix + - redo the menu for touch :-( + - test on smaller ios + ipad + - check this page: https://medium.com/appscope/designing-native-like-progressive-web-apps-for-ios-1b3cdda1d0e8 + - sharing idea: shareable stats page powered with some persistence / random playerid - bowling beast 8-ball character - more characters? diff --git a/dist/index.css b/dist/index.css index 290b3eb..b5248fc 100644 --- a/dist/index.css +++ b/dist/index.css @@ -40,12 +40,6 @@ body.in-game #game-canvas-wrapper { display: block; } -.phone-warning { - font-size: 20px; - text-align: center; - padding: 20px; -} - /* General Site Style sTuff */ .navbar-brand, .navbar-brand:hover, diff --git a/src/backend/views/game.toffee b/src/backend/views/game.toffee index 54082b2..02c8995 100644 --- a/src/backend/views/game.toffee +++ b/src/backend/views/game.toffee @@ -2,15 +2,6 @@
- - - -
diff --git a/src/backend/views/site-header.toffee b/src/backend/views/site-header.toffee index 4466051..ff55d6d 100644 --- a/src/backend/views/site-header.toffee +++ b/src/backend/views/site-header.toffee @@ -3,11 +3,11 @@ - + Tippy Coco - + {# if includeGame {:
diff --git a/src/frontend/display.ts b/src/frontend/display.ts index 13ffc13..95207f4 100644 --- a/src/frontend/display.ts +++ b/src/frontend/display.ts @@ -252,17 +252,8 @@ class Display { this.drawPlayerShadowBehind(playerRight) this.drawKapows(kapowManager) - for (const player of [this.game.playerLeft, this.game.playerRight]) { - let closestBall = this.game.balls[0] - let closestDistance = Infinity - for (const ball of this.game.balls) { - const distance = vec.lenSq(vec.sub(ball.physics.center, player.physics.center)) - if (distance < closestDistance) { - closestDistance = distance - closestBall = ball - } - } + const closestBall = this.game.getClosestBall(player.physics.center) this.drawPlayer(gameTime, player, closestBall) } this.game.balls.forEach((b) => this.drawBall(b)) diff --git a/src/frontend/game.ts b/src/frontend/game.ts index 45b6cb1..95eba7c 100644 --- a/src/frontend/game.ts +++ b/src/frontend/game.ts @@ -416,19 +416,33 @@ class Game { const player = this.player(playerSide) if (player.species === PlayerSpecies.Human) { - player.targetXVel = player.isDashing ? player.physics.vel.x : 0 + if (this.input.isPlayingWithTouch) { + if (player.isInJumpPosition) player.targetXVel = 0 + else player.targetXVel = player.physics.vel.x + } else { + player.targetXVel = player.isDashing ? player.physics.vel.x : 0 + } // the following is -1...1 and maps to 0 if near the center, as determined // in tweakables.thumbstickCenterTolerance const thumbstickPos = this.input.getLeftThumbStickX(playerSide) + const touchMoveX = this.input.getTouchThumbstickX() if (!player.isDashing) { if (this.input.isLeftPressed(playerSide)) player.moveLeft() else if (this.input.isRightPressed(playerSide)) player.moveRight() else if (thumbstickPos) player.moveRationally(thumbstickPos) + else if (touchMoveX) player.moveRationally(touchMoveX) if (player.canDashNow && this.input.wasDashJustPushed(playerSide)) { const dashDir = this.getDashDir(player) player.dash(dashDir) this.sound.play('dash', 1, 0, 0, false) - } else if (player.isInJumpPosition && this.input.isJumpPressed(playerSide)) player.jump() + } else if (player.isInJumpPosition) { + if (this.input.isJumpPressed(playerSide)) player.jump() + else if (this.input.didJumpViaTouch()) { + const b = this.getClosestBall(player.physics.center) + const dir = vec.normalized(vec.sub(b.physics.center, player.physics.center)) + player.jumpTowards(dir) + } + } } // triggers only register over some threshold as dtermined in tweakables.triggerTolerance const lTrigger = this.input.getTrigger(playerSide, 'left') @@ -609,6 +623,19 @@ class Game { } } + public getClosestBall(p: Vector2): Ball { + let closestBall = this.balls[0] + let closestDSq = Infinity + for (const ball of this.balls) { + const dSq = vec.lenSq(vec.sub(ball.physics.center, p)) + if (dSq < closestDSq) { + closestDSq = dSq + closestBall = ball + } + } + return closestBall + } + // keeps balls within the flowers; this is a simple/fast solution // to the problem of a multi-object pileup that could otherwise push them through. // Also, if we were to allow a y-velocity so high they could go over the flowers, diff --git a/src/frontend/input.ts b/src/frontend/input.ts index 27c2bf1..b79ddb9 100644 --- a/src/frontend/input.ts +++ b/src/frontend/input.ts @@ -3,13 +3,17 @@ import {GamepadConnectSummary, GamepadMonitor, TriggerName} from './gamepad-moni import {KeyboardMonitor} from './keyboard-monitor' import {MenuOwnership} from './menu' import {PlayerSpecies} from './player' +import {ScreenSide, TouchDeviceMonitor, TouchMoveDelta} from './touch-device-monitor' import tweakables from './tweakables' import {MenuSelectResult, PlayerSide, Vector2} from './types' +import {vec} from './utils' class Input { private pads = new GamepadMonitor() private keyboard = new KeyboardMonitor() + private touch = new TouchDeviceMonitor() private game: Game + private _isPlayingWithTouch = false public constructor(game: Game) { this.game = game @@ -17,8 +21,11 @@ class Input { public updateInputStates(): void { this.keyboard.update() this.pads.update() + this.touch.update() + } + public get isPlayingWithTouch() { + return this._isPlayingWithTouch } - public isKeyboardConnected(): boolean { return true // for now } @@ -47,7 +54,10 @@ class Input { byPlayerSide: null, byKeyboard: false, } - if (this.keyboard.anyKeysJustPushed(['Enter', 'Space'])) { + if (this.touch.wasScreenJustTapped()) { + res.selected = true + res.byKeyboard = true + } else if (this.keyboard.anyKeysJustPushed(['Enter', 'Space'])) { res.selected = true res.byKeyboard = true } else { @@ -121,11 +131,30 @@ class Input { const set = this.getKeyboardSet(pI) return this.keyboard.anyKeyDown(set.dash) || this.pads.anyButtonDown(pI, ['psSquare']) } - public isJumpPressed(pI: PlayerSide): boolean { const set = this.getKeyboardSet(pI) return this.keyboard.anyKeyDown(set.jump) || this.pads.anyButtonDown(pI, ['psX']) } + public didJumpViaTouch(): boolean { + const res = !!this.touch.anyTap(ScreenSide.Right) + this._isPlayingWithTouch ||= res + return res + } + public getTouchThumbstickX(): number { + const t = this.touch.anyDragMovement(ScreenSide.Left) + if (!t) return 0 + else { + this._isPlayingWithTouch = true + const x = t.vAsScreenFrac.x * tweakables.touch.xMoveMult + return Math.max(-1, Math.min(1, x)) + } + } + /* + public isPulledBackTouch(): TouchMoveDelta | false { + const res = this.touch.isPulledBack() + if (res) this._isPlayingWithTouch = true + return res + }*/ /** * returns 0 if trigger near 0, within tolerance * defined in tweakables. otherwise returns value up to 1 diff --git a/src/frontend/player.ts b/src/frontend/player.ts index 6cd1a0d..9c68ea0 100644 --- a/src/frontend/player.ts +++ b/src/frontend/player.ts @@ -102,9 +102,17 @@ class Player { } return false } - + public jumpTowards(dir: Vector2): boolean { + if (this.isInJumpPosition) { + this._jumpCount++ + this.physics.vel.y = this.jumpSpeed * dir.y + this.physics.vel.x = this.jumpSpeed * dir.x + return true + } + return false + } public dash(dir: Vector2) { - console.log('player::dash', this._isDashing) + console.log('player::dash', this._isDashing, 'can dash=', this.canDashNow) if (this.canDashNow) { this._isDashing = true this.physics.density = 234 diff --git a/src/frontend/site/index.ts b/src/frontend/site/index.ts index d125142..73365ab 100644 --- a/src/frontend/site/index.ts +++ b/src/frontend/site/index.ts @@ -14,7 +14,6 @@ class Site { await timeout(250) // all we are saying...is give paint a chance console.log('running. isMobileDevice=', isMobile()) if ($('#game-canvas-wrapper')) { - if (isMobile()) $('.phone-warning').style.display = '' this.loadGame() } } @@ -22,6 +21,7 @@ class Site { const btnEraseStorage = $('#btn-erase-storage') btnEraseStorage?.addEventListener('click', () => this.eraseLocalStorage()) window.addEventListener('keydown', (e: KeyboardEvent) => this.keyDown(e)) + window.addEventListener('touchend', (e: TouchEvent) => this.endTouch(e)) } private eraseLocalStorage() { if (confirm('Are you sure? This will clear your game stats.')) { @@ -50,5 +50,9 @@ class Site { private keyDown(e: KeyboardEvent) { if (e.key === ' ') this.launchGame() } + private endTouch(e: TouchEvent) { + console.log(e) + this.launchGame() + } } export {Site} diff --git a/src/frontend/touch-device-monitor.ts b/src/frontend/touch-device-monitor.ts new file mode 100644 index 0000000..6803c5a --- /dev/null +++ b/src/frontend/touch-device-monitor.ts @@ -0,0 +1,180 @@ +import tweakables from './tweakables' +import {Vector2} from './types' +import {vec} from './utils' + +type TouchIdentifier = number + +enum ScreenSide { + Left = 'Left', + Right = 'Right', +} +type TouchDetail = { + screenX: number + screenY: number + identifier: TouchIdentifier + whenNoted: number +} +type TouchMoveDelta = { + v: Vector2 + ms: number + dist: number + normalizedDir: Vector2 + vAsScreenFrac: Vector2 + newPos: Vector2 +} + +type ActiveTouchList = {[k: TouchIdentifier]: TouchDetail} + +interface TouchDeviceState { + liveTapCount: number + touches: ActiveTouchList +} + +class TouchDeviceMonitor { + private currState: TouchDeviceState + private prevState: TouchDeviceState + private touchStartStates: ActiveTouchList = {} + + constructor() { + this.currState = {liveTapCount: 0, touches: {}} + this.prevState = {liveTapCount: 0, touches: {}} + window.addEventListener('touchstart', (e: Event) => this.touchStart(e as TouchEvent)) + window.addEventListener('touchend', (e: Event) => this.touchEnd(e as TouchEvent)) + window.addEventListener('touchmove', (e: Event) => this.touchMove(e as TouchEvent)) + } + private get screenDiagonalLen() { + const w = window.screen.width + const h = window.screen.height + return Math.sqrt(w * w + h * h) + } + private getScreenSide(t: TouchDetail): ScreenSide { + return t.screenX < window.screen.width / 2 ? ScreenSide.Left : ScreenSide.Right + } + private makeTouchDetail(t: Touch): TouchDetail { + return { + whenNoted: Date.now(), + screenX: t.screenX, + screenY: t.screenY, + identifier: t.identifier, + } + } + private touchStart(e: TouchEvent) { + console.log(`touchStart`, e) + this.currState.liveTapCount++ + for (const t of e.changedTouches) { + this.currState.touches[t.identifier] = this.makeTouchDetail(t) + this.touchStartStates[t.identifier] ||= this.makeTouchDetail(t) + } + } + private touchEnd(e: TouchEvent) { + console.log(`touchEnd`, e) + this.currState.liveTapCount-- + for (const t of e.changedTouches) { + delete this.currState.touches[t.identifier] + this.touchStartStates[t.identifier] ||= this.makeTouchDetail(t) + } + } + private touchMove(e: TouchEvent) { + //console.log(`touchMove`, e) + for (const t of e.changedTouches) { + this.currState.touches[t.identifier] = this.makeTouchDetail(t) + this.touchStartStates[t.identifier] ||= this.makeTouchDetail(t) + } + } + public wasScreenJustTapped() { + return this.currState.liveTapCount < this.prevState.liveTapCount + } + + private positionDelta(t1: TouchDetail, t2: TouchDetail) { + const ms = t2.whenNoted - t1.whenNoted + const x = t2.screenX - t1.screenX + const y = -(t2.screenY - t1.screenY) + const v = {x: x, y: y} + const dist = vec.len(v) + const fracScreenLen = dist / this.screenDiagonalLen + const normalizedDir = vec.normalized(v) + const vAsScreenFrac = vec.scale(normalizedDir, fracScreenLen) + const newPos = {x: t2.screenX, y: t2.screenY} + return {v, ms, dist, normalizedDir, vAsScreenFrac, newPos} + } + + /** + * has the player pulled back to release (in effect, are they currently dragging, + * and if so, which dir) + */ + /* + public isPulledBack(): TouchMoveDelta | false { + const tt = tweakables.touch + if (this.currState.liveTapCount >= 1 && this.currState.liveTapCount === this.prevState.liveTapCount) { + for (const t of Object.values(this.currState.touches)) { + if (this.prevState.touches[t.identifier]) { + const start = this.touchStartStates[t.identifier] + const delta = this.positionDelta(start, t) + if (delta.ms <= tt.flickMaxMs && delta.ms >= tt.flickMinMs && delta.dist > tt.flickMinDistPx * tt.flickMinDistPx) { + return delta + } + } + } + } + return false + }*/ + + public anyDragMovement(screenSide: ScreenSide): TouchMoveDelta | false { + const deltas: TouchMoveDelta[] = [] + for (const curr of Object.values(this.currState.touches)) { + if (this.getScreenSide(curr) === screenSide) { + const start = this.touchStartStates[curr.identifier] + const delta = this.positionDelta(start, curr) + deltas.push(delta) + } + } + deltas.sort((d1, d2) => Math.abs(d2.dist) - Math.abs(d1.dist)) + return deltas[0] ?? false + } + + public anyTap(screenSide: ScreenSide): TouchMoveDelta | false { + const tt = tweakables.touch + if (this.currState.liveTapCount < this.prevState.liveTapCount) { + // ok, so a touch did disappear...let's see if it was a tap + for (const t of Object.values(this.prevState.touches)) { + if (this.getScreenSide(t) === screenSide) { + if (!this.currState.touches[t.identifier]) { + const start = this.touchStartStates[t.identifier] + const delta = this.positionDelta(start, t) + if (delta.ms <= tt.tapMaxDist && delta.dist < tt.tapMaxDist) { + return delta + } + } + } + } + } + return false + } + + /** + * this is how the player does a dash or jump + * returns the the direction they are dashing or jumping, with magnitude retained + */ + public didPlayerReleasePullback(): TouchMoveDelta | false { + const tt = tweakables.touch + if (this.currState.liveTapCount < this.prevState.liveTapCount) { + // ok, so a touch did disappear...let's find + for (const t of Object.values(this.prevState.touches)) { + if (!this.currState.touches[t.identifier]) { + const start = this.touchStartStates[t.identifier] + const delta = this.positionDelta(start, t) + if (delta.ms <= tt.flickMaxMs && delta.ms >= tt.flickMinMs && delta.dist > tt.flickMinDistPx) { + return delta + } + } + } + } + return false + } + + public update() { + this.prevState = {liveTapCount: this.currState.liveTapCount, touches: {}} + this.prevState.touches = Object.assign({}, this.currState.touches) + } +} +export {TouchDeviceMonitor, TouchMoveDelta, ScreenSide} diff --git a/src/frontend/tweakables.ts b/src/frontend/tweakables.ts index b5328b8..758596f 100644 --- a/src/frontend/tweakables.ts +++ b/src/frontend/tweakables.ts @@ -75,6 +75,14 @@ export default { courtWidth, twoPlayerControls, onePlayerControls, + touch: { + flickMinDistPx: 30, + flickMinMs: 20, + flickMaxMs: 10000, + tapMaxMs: 100, + tapMaxDist: 20, + xMoveMult: 10, + }, physics: { ballPlayerElasticity: 0.95, ballAngularFriction: 0.5,