diff --git a/src/client/game.ts b/src/client/game.ts index d00648d..3ea43f1 100644 --- a/src/client/game.ts +++ b/src/client/game.ts @@ -1,6 +1,6 @@ import { Socket } from 'socket.io-client'; import { Color, DoubleSide, MeshBasicMaterial, Vector3 } from 'three'; -import { isMobileOrTablet } from '../common/helper'; +import { clamp, isMobileOrTablet } from '../common/helper'; import { CharacterPacket, CompositePacket } from '../common/types/packet'; import { IcyNetUser } from '../common/types/user'; import { ThirdPersonCamera } from './object/camera'; @@ -30,6 +30,7 @@ export class Game { private _loading = LoadingManagerWrapper.getInstance(); private character: CharacterPacket = {}; private party: string[] = []; + private _locked = false; public world!: ClientWorld; public renderer = new Renderer(); @@ -149,6 +150,19 @@ export class Game { this.renderer.scene.add(flowers); this.renderer.scene.add(flowers2); this._loading.isConnecting(); + + window.addEventListener('keyup', (ev) => { + if (ev.key === 'Shift') { + this.toggleCamLock(); + } + }); + } + + public toggleCamLock(): boolean { + this._locked = !this._locked; + this.thirdPersonCamera?.setLock(this._locked); + this.player?.setCameraLock(this._locked); + return this._locked; } public dispose() { @@ -207,9 +221,18 @@ export class Game { this.renderer.canvas, ); this.thirdPersonCamera.initialize(); + this.thirdPersonCamera.registerAltMoveFunction((x, y) => { + this.player.angularVelocity.set( + 0, + clamp(x * 0.5, -Math.PI, Math.PI), + 0, + ); + }); this.joystick = new Joystick(player); this.joystick.initialize(); + this.joystick.addButton(-60, -20, 'LOCK', () => this.toggleCamLock()); + this.joystick.addButton(135, -20, 'JUMP', () => this.player.jump()); if (isMobileOrTablet()) { this.joystick.show(); diff --git a/src/client/object/camera.ts b/src/client/object/camera.ts index eac557e..0b7a749 100644 --- a/src/client/object/camera.ts +++ b/src/client/object/camera.ts @@ -21,8 +21,11 @@ export class ThirdPersonCamera { private panning = false; private pinching = false; + private locked = false; private previousPinchLength = 0; + private _moveFn?: (x: number, y: number) => void; + constructor( private camera: PerspectiveCamera, private target: Object3D, @@ -32,10 +35,16 @@ export class ThirdPersonCamera { private dragEvent = (x: number, y: number) => { this.prevMousePos.copy(this.mousePos); this.mousePos = this.mousePos.fromArray([x, y]); + const offset = this.prevMousePos.clone().sub(this.mousePos); + + if (this.locked) { + if (this._moveFn && this.panning) { + this._moveFn(offset.x, offset.y); + } + return; + } if (this.panning) { - const offset = this.prevMousePos.clone().sub(this.mousePos); - this.angleAroundPlayer = this.angleAroundPlayer + ((offset.x * 0.3) % 360); this.pitch = clamp(this.pitch + offset.y * 0.3, -90, 90); @@ -52,12 +61,21 @@ export class ThirdPersonCamera { } }, mouseup: (e: MouseEvent) => { + const wasPanning = this.panning === true; if (e.button === 2) { this.panning = false; } + + if (wasPanning && this.locked && this._moveFn && !this.panning) { + this._moveFn(0, 0); + } }, mouseleave: (e: MouseEvent) => { + const wasPanning = this.panning === true; this.panning = false; + if (wasPanning && this.locked && this._moveFn && !this.panning) { + this._moveFn(0, 0); + } }, mousemove: (e: MouseEvent) => this.dragEvent(e.clientX, e.clientY), wheel: (e: WheelEvent) => { @@ -98,9 +116,14 @@ export class ThirdPersonCamera { } }, touchend: (ev: TouchEvent) => { + const wasPanning = this.panning === true; this.pinching = false; this.panning = false; this.previousPinchLength = 0; + + if (wasPanning && this.locked && this._moveFn && !this.panning) { + this._moveFn(0, 0); + } }, }; @@ -131,6 +154,14 @@ export class ThirdPersonCamera { this.camera.lookAt(this.currentLookAt); } + public setLock(isLocked: boolean) { + this.locked = isLocked; + } + + public registerAltMoveFunction(fn: (x: number, y: number) => void) { + this._moveFn = fn; + } + private calculateCameraOffset() { const hdist = this.distance * Math.cos(deg2rad(this.pitch)); const vdist = this.distance * Math.sin(deg2rad(this.pitch)); diff --git a/src/client/object/joystick.ts b/src/client/object/joystick.ts index b868b82..7006f80 100644 --- a/src/client/object/joystick.ts +++ b/src/client/object/joystick.ts @@ -3,6 +3,7 @@ import { Player } from './player'; export class Joystick { public element = document.createElement('div'); + private _inner = document.createElement('div'); private knob = document.createElement('div'); private mousePos = new Vector2(); @@ -20,19 +21,21 @@ export class Joystick { constructor(private player: Player, public radius = 60) {} initialize() { - this.element.classList.add('joystick', 'joystick__wrapper'); + this.element.classList.add('joystick__wrapper'); + this._inner.classList.add('joystick'); this.knob.classList.add('joystick__knob'); - this.element.append(this.knob); + this._inner.append(this.knob); + this.element.append(this._inner); document.body.append(this.element); - this.element.addEventListener('touchstart', (e) => { + this._inner.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0] || e.changedTouches[0]; this.mousePos.fromArray([touch.pageX, touch.pageY]); this.dragging = true; }); - this.element.addEventListener('touchmove', (e) => { + this._inner.addEventListener('touchmove', (e) => { e.preventDefault(); if (this.dragging) { this.prevMousePos.copy(this.mousePos); @@ -49,7 +52,7 @@ export class Joystick { } }); - this.element.addEventListener('touchend', (e) => { + this._inner.addEventListener('touchend', (e) => { this.dragging = false; this.centerKnob(); }); @@ -93,6 +96,32 @@ export class Joystick { this.reset(); } + public addButton( + x: number, + y: number, + text: string, + toggle: () => void | boolean, + ) { + const btn = document.createElement('button'); + btn.innerText = text; + btn.style.setProperty('--button-x', `${x}px`); + btn.style.setProperty('--button-y', `${y}px`); + btn.classList.add('joystick__button'); + btn.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + const value = toggle(); + if (typeof value === 'boolean') { + if (value) { + btn.classList.add('joystick__button--active'); + } else { + btn.classList.remove('joystick__button--active'); + } + } + }); + this.element.append(btn); + } + private _windowEvent() { this.reset(); } diff --git a/src/client/object/player.ts b/src/client/object/player.ts index 46dfc03..751fd1b 100644 --- a/src/client/object/player.ts +++ b/src/client/object/player.ts @@ -38,6 +38,11 @@ export class Player extends PonyEntity { */ private _wasTurning = false; + /** + * When true, left and right movement will not rotate, instead will move. + */ + private _cameraLock = false; + constructor(public user: IcyNetUser) { super(); } @@ -84,7 +89,7 @@ export class Player extends PonyEntity { vector.copy(this._direction); } - if (vector.x !== 0) { + if (vector.x !== 0 && !this._cameraLock) { this.angularVelocity.set(0, Math.PI * vector.x, 0); this.changes.angular = this.angularVelocity.toArray(); @@ -96,10 +101,20 @@ export class Player extends PonyEntity { this.changes.rotation = this.container.rotation.toArray(); } - if (vector.y !== 0) { + if (vector.y !== 0 || (this._cameraLock && vector.x !== 0)) { const directional = this._lookVector .clone() .multiplyScalar(vector.y * 2.5); + + if (this._cameraLock && vector.x !== 0) { + const sideways = new Vector3(); + sideways.copy(this._lookVector); + sideways.applyAxisAngle(new Vector3(0, 1, 0), Math.PI / 2); + sideways.normalize(); + sideways.multiplyScalar(vector.x * 2.5); + directional.add(sideways); + } + this.velocity.set(directional.x, this.velocity.y, directional.z); this.changes.velocity = this.velocity.toArray(); @@ -145,4 +160,8 @@ export class Player extends PonyEntity { this._prevKeydownMap = { ...this.keydownMap }; } + + public setCameraLock(isLocked: boolean) { + this._cameraLock = isLocked; + } } diff --git a/src/client/scss/index.scss b/src/client/scss/index.scss index 011720f..e182f76 100644 --- a/src/client/scss/index.scss +++ b/src/client/scss/index.scss @@ -14,17 +14,20 @@ body { } .joystick { - display: none; - position: absolute; width: var(--size); height: var(--size); - bottom: 10px; - left: calc(50% - var(--size) / 2); background-color: #e7e7e7b3; border-radius: 100%; border: 2px solid #ddd; z-index: 1000; + &__wrapper { + display: none; + position: absolute; + left: calc(50% - var(--size) / 2); + bottom: 10px; + } + &__knob { width: calc(var(--size) / 2); height: calc(var(--size) / 2); @@ -35,6 +38,32 @@ body { cursor:grab; transform-origin: center center; } + + &__button { + position: absolute; + appearance: none; + left: var(--button-x); + top: var(--button-y); + width: 48px; + height: 48px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 100%; + background-color: rgba(231, 231, 231, 0.7019607843); + border: 2px solid #ddd; + z-index: 1001; + color: #5c5c5c; + font-weight: bold; + transition: border 250ms linear, background-color 250ms linear; + + &--active { + border-width: 4px; + border-color: #db8f00; + background-color: rgb(231 231 231 / 78%); + } + } } .chat {