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,