diff --git a/src/client/game.ts b/src/client/game.ts index 31fb95f..afaad62 100644 --- a/src/client/game.ts +++ b/src/client/game.ts @@ -96,7 +96,7 @@ export class Game { 8, 0.5, 8, - this.world.getInterpolatedHeight.bind(this.world), + this.world.getHeight.bind(this.world), ); const flowerfield = Grass.getInstance().createGrassPatch( @@ -104,7 +104,7 @@ export class Game { 8, 4, 3, - this.world.getInterpolatedHeight.bind(this.world), + this.world.getHeight.bind(this.world), ); const flowerfield2 = Grass.getInstance().createGrassPatch( @@ -112,7 +112,7 @@ export class Game { 4, 4, 3, - this.world.getInterpolatedHeight.bind(this.world), + this.world.getHeight.bind(this.world), ); const grass = Grass.getInstance().createInstance( diff --git a/src/client/object/canvas-utils.ts b/src/client/object/canvas-utils.ts index 4fe64f2..ccacaeb 100644 --- a/src/client/object/canvas-utils.ts +++ b/src/client/object/canvas-utils.ts @@ -1,4 +1,5 @@ import { CanvasTexture, LinearFilter, ClampToEdgeWrapping } from 'three'; +import { rgbToHex, to1D } from '../../common/convert'; /** * Draws a rounded rectangle using the current state of the canvas. @@ -172,4 +173,49 @@ export class CanvasUtils { return { texture, width, height }; } + + public readPixelDataRGB(image: HTMLImageElement): number[] { + const array = new Array(image.width * image.height); + const ctx = document.createElement('canvas').getContext('2d'); + ctx.canvas.width = image.width; + ctx.canvas.height = image.height; + ctx.drawImage(image, 0, 0, image.width, image.height); + + // pixel data + const data = ctx.getImageData(0, 0, image.width, image.height); + for (let x = 0; x < image.width; x++) { + for (let y = 0; y < image.height; y++) { + const index = to1D(x, y, image.width); + array[index] = rgbToHex( + data.data[index * 4], + data.data[index * 4 + 1], + data.data[index * 4 + 2], + ); + } + } + + return array; + } + + public readPixelDataRScaled( + image: HTMLImageElement, + scale: number, + ): number[] { + const array = new Array(image.width * image.height); + const ctx = document.createElement('canvas').getContext('2d'); + ctx.canvas.width = image.width; + ctx.canvas.height = image.height; + ctx.drawImage(image, 0, 0, image.width, image.height); + + // pixel data + const data = ctx.getImageData(0, 0, image.width, image.height); + for (let x = 0; x < image.width; x++) { + for (let y = 0; y < image.height; y++) { + const index = to1D(x, y, image.width); + array[index] = (data.data[index * 4] * scale) / 255; + } + } + + return array; + } } diff --git a/src/client/object/model/pony.ts b/src/client/object/model/pony.ts index 63df93b..0ace9d5 100644 --- a/src/client/object/model/pony.ts +++ b/src/client/object/model/pony.ts @@ -28,6 +28,10 @@ const nameTagBuilder = new CanvasUtils({ export class PonyEntity { public velocity = new Vector3(0, 0, 0); public angularVelocity = new Vector3(0, 0, 0); + public onFloor = true; + public remote = false; + public gravity = -50; + public jumpPower = 16; public mixer!: AnimationMixer; public container!: Object3D; public model!: Object3D; @@ -58,6 +62,14 @@ export class PonyEntity { } update(dt: number) { + this.mixer.update(dt); + + if (this.remote) { + return; + } + + this.velocity.y += this.gravity * dt; + this.container.position.add(this.velocity.clone().multiplyScalar(dt)); this.container.rotation.setFromVector3( new Vector3( @@ -69,13 +81,18 @@ export class PonyEntity { // Put pony on the terrain const terrainFloorHeight = - this.heightSource?.getInterpolatedHeight( + this.heightSource?.getHeight( this.container.position.x, this.container.position.z, ) || 0; - this.container.position.y = terrainFloorHeight; - this.mixer.update(dt); + if (this.container.position.y <= terrainFloorHeight) { + this.onFloor = true; + this.velocity.y = 0; + this.container.position.y = terrainFloorHeight; + } else { + this.onFloor = false; + } } dispose() { @@ -106,6 +123,10 @@ export class PonyEntity { } } + public jump() { + this.velocity.y = 16; + } + public setColor(color: number | string) { this.material.color = new Color(color); } diff --git a/src/client/object/player-entity.ts b/src/client/object/player-entity.ts index 9d18ae8..79010a3 100644 --- a/src/client/object/player-entity.ts +++ b/src/client/object/player-entity.ts @@ -18,6 +18,7 @@ export class PlayerEntity extends PonyEntity { private _targetFrame: any = null; private _lastFrame: any = null; private _chats: ChatBubble[] = []; + public remote = true; private _p1 = new Vector3(); private _p2 = new Vector3(); diff --git a/src/client/object/player.ts b/src/client/object/player.ts index 20e8982..46dfc03 100644 --- a/src/client/object/player.ts +++ b/src/client/object/player.ts @@ -97,14 +97,17 @@ export class Player extends PonyEntity { } if (vector.y !== 0) { - this.velocity.copy(this._lookVector.clone().multiplyScalar(vector.y * 5)); + const directional = this._lookVector + .clone() + .multiplyScalar(vector.y * 2.5); + this.velocity.set(directional.x, this.velocity.y, directional.z); this.changes.velocity = this.velocity.toArray(); this._wasMoving = true; this.setWalkAnimationState(1); } else if (this._wasMoving && !wasExternalForce) { this._wasMoving = false; - this.velocity.set(0, 0, 0); + this.velocity.set(0, this.velocity.y, 0); this.changes.velocity = this.velocity.toArray(); this.changes.position = this.container.position.toArray(); this.setWalkAnimationState(0); @@ -133,6 +136,10 @@ export class Player extends PonyEntity { this._direction.x = 0; } + if (this.keydownMap[' '] && !this._prevKeydownMap[' '] && this.onFloor) { + this.jump(); + } + this.moveCharacter(dt); super.update(dt); diff --git a/src/client/object/world/ClientWorld.ts b/src/client/object/world/ClientWorld.ts index bc572c2..f0319ae 100644 --- a/src/client/object/world/ClientWorld.ts +++ b/src/client/object/world/ClientWorld.ts @@ -17,10 +17,10 @@ export class ClientWorld extends WorldManager { private _shader = new ClientWorldChunkShader(this._worldTextures); getNormalVector(x: number, y: number): Vector3 { - const heightL = this.getHeight(x - 1, y); - const heightR = this.getHeight(x + 1, y); - const heightD = this.getHeight(x, y - 1); - const heightU = this.getHeight(x, y + 1); + const heightL = this.getPointHeight(x - 1, y); + const heightR = this.getPointHeight(x + 1, y); + const heightD = this.getPointHeight(x, y - 1); + const heightU = this.getPointHeight(x, y + 1); const normalized = new Vector3(heightL - heightR, 2, heightD - heightU); normalized.normalize(); return normalized; @@ -75,7 +75,7 @@ export class ClientWorld extends WorldManager { material, ); - root.getHeight = this.getInterpolatedHeight.bind(this); + root.getHeight = this.getHeight.bind(this); root.getNormal = this.getNormalVector.bind(this); root.initialize(); diff --git a/src/client/object/world/ClientWorldLoader.ts b/src/client/object/world/ClientWorldLoader.ts index 7a0d8aa..b8b2a61 100644 --- a/src/client/object/world/ClientWorldLoader.ts +++ b/src/client/object/world/ClientWorldLoader.ts @@ -1,9 +1,10 @@ import { ImageLoader } from 'three'; -import { to1D } from '../../../common/convert'; import { WorldLoader } from '../../../common/world/WorldLoader'; +import { CanvasUtils } from '../canvas-utils'; const loader = new ImageLoader(); -const worldPath = '/assets/terrain/region/'; +const canvasUtil = new CanvasUtils(); +const worldPath = '/assets/terrain/region'; export class ClientWorldLoader implements WorldLoader { async loadHeightMap( @@ -14,7 +15,7 @@ export class ClientWorldLoader implements WorldLoader { return new Promise((resolve, reject) => { loader.load( `${worldPath}/height-${chunkX}-${chunkY}.png`, - (data) => resolve(ClientWorldLoader.heightFromImage(data, scale)), + (data) => resolve(canvasUtil.readPixelDataRScaled(data, scale)), undefined, (err) => { reject(err); @@ -22,26 +23,4 @@ export class ClientWorldLoader implements WorldLoader { ); }); } - - public static heightFromImage( - image: HTMLImageElement, - scale: number, - ): number[] { - const array = new Array(image.width * image.height); - const ctx = document.createElement('canvas').getContext('2d'); - ctx.canvas.width = image.width; - ctx.canvas.height = image.height; - ctx.drawImage(image, 0, 0, image.width, image.height); - - // pixel data - const data = ctx.getImageData(0, 0, image.width, image.height); - for (let x = 0; x < image.width; x++) { - for (let y = 0; y < image.height; y++) { - const index = to1D(x, y, image.width); - array[index] = (data.data[index * 4] * scale) / 255; - } - } - - return array; - } } diff --git a/src/client/object/world/quadtree/quadtree-node.ts b/src/client/object/world/quadtree/quadtree-node.ts index a5655a0..9ee61f2 100644 --- a/src/client/object/world/quadtree/quadtree-node.ts +++ b/src/client/object/world/quadtree/quadtree-node.ts @@ -73,7 +73,7 @@ export class QuadtreeNode { ); if (this._leaf) { - if (abs.distanceTo(camera) < size && this._canSubdivide()) { + if (this._canSubdivide() && abs.distanceTo(camera) < size) { this._subdivide(); this.root.actionsLeft -= 1; return; diff --git a/src/common/convert.ts b/src/common/convert.ts index a2e8e97..878c987 100644 --- a/src/common/convert.ts +++ b/src/common/convert.ts @@ -6,6 +6,10 @@ export function convertHex(hex: number): { r: number; g: number; b: number } { }; } +export function rgbToHex(r: number, g: number, b: number): number { + return (1 << 24) + (r << 16) + (g << 8) + b; +} + export function hexToString(hex: number): string { const { r, g, b } = convertHex(hex); return `#${r.toString(16).padStart(2, '0')}${g diff --git a/src/common/world/WorldManager.ts b/src/common/world/WorldManager.ts index e8ebaa6..676d9c0 100644 --- a/src/common/world/WorldManager.ts +++ b/src/common/world/WorldManager.ts @@ -46,7 +46,7 @@ export class WorldManager { } } - getHeight(x: number, y: number): number { + getPointHeight(x: number, y: number): number { const chunkX = Math.floor(x / this.worldChunkSize); const chunkY = Math.floor(y / this.worldChunkSize); if ( @@ -64,7 +64,7 @@ export class WorldManager { ); } - getInterpolatedHeight(x: number, y: number): number { + getHeight(x: number, y: number): number { const chunkX = Math.floor(x / this.worldChunkSize); const chunkY = Math.floor(y / this.worldChunkSize); if (