diff --git a/packages/client/src/Renderer.ts b/packages/client/src/Renderer.ts index 7631e9ab..f2a04066 100644 --- a/packages/client/src/Renderer.ts +++ b/packages/client/src/Renderer.ts @@ -8,320 +8,71 @@ import { RpgClientEntryPointOptions } from "./clientEntryPoint"; const { elementToPositionAbsolute } = Utils; export enum TransitionMode { - None, - Fading + None, + Fading, } -enum ContainerName { - Map = 'map' -} - -export const EVENTS_MAP = { - MouseEvent: ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu', 'wheel'], - KeyboardEvent: ['keydown', 'keyup', 'keypress', 'keydownoutside', 'keyupoutside', 'keypressoutside'], - PointerEvent: ['pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointercancel'], - TouchEvent: ['touchstart', 'touchend', 'touchmove', 'touchcancel'] +export type Scenes = { + [key: string]: (...props) => any; }; export class RpgRenderer { - private gameEngine: GameEngineClient = this.context.inject(GameEngineClient) - private clientEngine: RpgClientEngine = this.context.inject(RpgClientEngine) - - public vm: ComponentPublicInstance - public app: App - public readonly stage: Container = new Container() - private readonly sceneContainer: Container = new Container() - private readonly fadeContainer: Graphics = new Graphics() - private readonly spinner: SpinnerGraphic = new SpinnerGraphic(this.clientEngine) - public options: any = {} - public guiEl: HTMLDivElement - - private scene: Scene | null = null - private renderer: IRenderer - private _width: number = 800 - private _height: number = 400 - private canvasEl: HTMLElement - private selector: HTMLElement - private loadingScene = { - transitionIn: new Subject(), - transitionOut: new Subject() - } - private freeze: boolean = false - private prevObjectScene: any = {} - public transitionMode: TransitionMode = TransitionMode.Fading - - constructor(private context: InjectContext) { - this.clientEngine.tick.subscribe(({ timestamp, deltaRatio, frame, deltaTime }) => { - this.draw(timestamp, deltaTime, deltaRatio, frame) - }) - this.transitionCompleted() - } - - /** @internal */ - init(): Promise { - return this.onDOMLoaded() - } - - /** @internal */ - _resize(w: number, h: number) { - if (!w) w = this.options.canvas.width - if (!h) h = this.options.canvas.height - const scene = this.getScene() - if (this.scene && scene?.viewport) { - scene.viewport.screenWidth = w - scene.viewport.screenHeight = h - } - if (this.vm) { - this.vm.$el.style = `width:${w}px;height:${h}px` - } - this.renderer.resize(w, h) - this._width = w - this._height = h - this.fadeContainer.beginFill(0x00000) - this.fadeContainer.drawRect(0, 0, w, h) - this.fadeContainer.endFill() - this.spinner.x = w * 0.5 - this.spinner.y = h * 0.5 - } - - get canvas(): HTMLCanvasElement { - return this.renderer.view as HTMLCanvasElement - } - - get height(): number { - return this._height - } - - set height(val: number) { - this._resize(this._width, val) - } - - get width(): number { - return this._width - } - - set width(val: number) { - this._resize(val, this.height) - } - - /** @internal */ - async onDOMLoaded(): Promise { - let options = { - antialias: true, - ...this.options.canvas - }; - this.renderer = autoDetectRenderer(options) - this.selector = document.body.querySelector(this.options.selector) - this.guiEl = this.selector.querySelector(this.options.selectorGui) - this.canvasEl = this.selector.querySelector(this.options.selectorCanvas) - - if (!this.guiEl) { - this.guiEl = document.createElement('div') - this.guiEl = this.selector.appendChild(this.guiEl) - } - - elementToPositionAbsolute(this.guiEl) - - if (!this.canvasEl) { - this.selector.insertBefore(this.renderer.view as HTMLCanvasElement, this.selector.firstChild) - const [canvas] = document.querySelector(this.options.selector).children - canvas.style.position = 'absolute' - } - else { - this.canvasEl.appendChild(this.renderer.view as HTMLCanvasElement) - } - - this.stage.addChild(this.sceneContainer) - this.stage.addChild(this.fadeContainer) - this.fadeContainer.addChild(this.spinner) - - this.fadeContainer.visible = false - this.fadeContainer.alpha = 0 - - await RpgGui._initialize(this.context, this.guiEl) - - this.resize() - this.bindMouseControls() - - } - - private bindMouseControls() { - const controlInstance = this.context.inject(KeyboardControls) - const controls = controlInstance.getControls() - for (let key in controls) { - const { actionName } = controls[key] - if (EVENTS_MAP.MouseEvent.includes(key)) { - this.canvas.addEventListener(key, (e) => { - controlInstance.applyControl(actionName) - }) - } - } - } - - /** @internal */ - resize() { - const size = () => { - const { offsetWidth, offsetHeight } = this.canvasEl || this.selector - this._resize(offsetWidth, offsetHeight) - RpgPlugin.emit(HookClient.WindowResize) - } - window.addEventListener('resize', size) - size() - } - - /** @internal */ - getScene(): T | null { - return this.scene as any - } - - /** @internal */ - draw(t: number, deltaTime: number, deltaRatio: number, frame: number) { - if (!this.renderer) return - if (this.scene && !this.freeze) this.scene.draw(t, deltaTime, deltaRatio, frame) - this.renderer.render(this.stage) - } - - /** @internal */ - async loadScene(name: string, obj) { - const scene = this.getScene() - if (scene && scene.data.id == obj.id) { - const container = await scene.load(obj, this.prevObjectScene, true) - this.sceneContainer.removeChildren() - this.sceneContainer.addChild(container) - scene.updateTilesOverlayAllSprites() - this.scene?.update() - return - } - this.loadingScene.transitionIn.next({ name, obj }) - this.loadingScene.transitionIn.complete() - } - - private async createScene(name: string, obj) { - const container = await this.getScene()?.load(obj, this.prevObjectScene) - this.prevObjectScene = { ...obj } - this.sceneContainer.children.forEach(child => { - if (child.name === ContainerName.Map) this.sceneContainer.removeChild(child) - }) - if (container) { - container.name = ContainerName.Map - this.sceneContainer.addChild(container) - } - this.scene?.update() - } - - /** @internal */ - transitionScene(name: string) { - this.freeze = true - this.fadeContainer.visible = true - RpgPlugin.emit(HookClient.BeforeSceneLoading, { - name - }) - this.clientEngine.controls.stopInputs() - const finish = () => { - this.clearScene() - this.loadingScene.transitionOut.next(name) - this.loadingScene.transitionOut.complete() - } - if (this.transitionMode == TransitionMode.Fading) { - new TransitionScene(this.context, this.fadeContainer) - .addFadeOut() - .onComplete(finish) - .start() - } - else { - finish() - } - } - - /** @internal */ - transitionCompleted() { - this.loadingScene = { - transitionIn: new Subject(), - transitionOut: new Subject() - } - this.clientEngine.roomJoin = new Subject() - forkJoin({ - in: this.loadingScene.transitionIn, - out: this.loadingScene.transitionOut, - room: this.clientEngine.roomJoin - }).subscribe(async (data: { in: any }) => { - const { in: { obj, name } } = data - const scenes = this.options.scenes || {} - switch (name) { - case PresetScene.Map: - const sceneClass = scenes[PresetScene.Map] || SceneMap - this.scene = new sceneClass(this.context, this.renderer, { - screenWidth: this.renderer.screen.width, - screenHeight: this.renderer.screen.height, - drawMap: this.options.drawMap - }) - break; - } - await this.createScene(name, obj) - this.freeze = false - const finish = () => { - this.clientEngine.controls.listenInputs() - this.fadeContainer.visible = false - this.transitionCompleted() - RpgPlugin.emit(HookClient.AfterSceneLoading, this.scene) - } - if (this.transitionMode == TransitionMode.Fading) { - new TransitionScene(this.context, this.fadeContainer) - .addFadeIn() - .onComplete(finish) - .start() - } - else { - finish() - } - }) - } - - /** @internal */ - clearScene() { - this.scene = null - this.sceneContainer.removeChildren() - } - - /** - * @title Propagate mouse event to Viewport - * @method propagateEvent(ev) - * @stability 1 - * @memberof RpgRenderer - * @returns {void} - */ - propagateEvent(ev: MouseEvent) { - const rect = this.canvas.getBoundingClientRect(); - const canvasX = rect.left + window.scrollX; - const canvasY = rect.top + window.scrollY; - const realX = ev.clientX - canvasX; - const realY = ev.clientY - canvasY; - const boundary = new EventBoundary(this.stage); - const event = new FederatedPointerEvent(boundary) - event.global.set(realX, realY); - event.type = ev.type; - const hitTestTarget = boundary.hitTest(realX, realY); - hitTestTarget?.dispatchEvent(event) - this.canvas.dispatchEvent(new MouseEvent(ev.type, ev)) - } - - /*** - * Propagate events from an HTMLElement to the canvas - * - * @title Propagate events - * @method addPropagateEventsFrom(el) - * @stability 1 - * @memberof RpgRenderer - * @returns {void} - */ - addPropagateEventsFrom(el: HTMLElement) { - for (let [_Constructor, events] of Object.entries(EVENTS_MAP)) { - for (let type of events) { - el.addEventListener(type, (e) => { - const _class = window[_Constructor] ?? MouseEvent - this.canvas.dispatchEvent(new _class(type, e)) - }); - } - } - } + private options!: RpgClientEntryPointOptions + private canvasEl: HTMLElement; + private selector: HTMLElement; + private currentSceneName = signal(""); + private currentSceneData = signal({}); + private scenes = {}; + + width = signal(800); + height = signal(600); + + public guiEl: HTMLDivElement; + + constructor(private context: Context) { + this.options = inject(context, ConfigToken) + } + + /** @internal */ + init(scenes: Scenes): Promise { + // this.scenes = { + // map: SceneMap, + // ...scenes, + // }; + return this.onDOMLoaded(); + } + + /** @internal */ + async onDOMLoaded(): Promise { + this.selector = document.body.querySelector(this.options?.selector ?? "#rpg") as HTMLElement; + + await bootstrapCanvas(this.selector , Canvas); + + // await h( + // Canvas, + // { + // canvasEl: this.canvasEl, + // selector: this.options.selector, + // width: this.width, + // height: this.height + // }, + // cond( + // computed(() => this.currentSceneName()), + // () => { + // const name = this.currentSceneName(); + // const sceneFn = this.scenes[name]; + // if (!sceneFn) { + // throw new Error(`Scene ${name} not found`); + // } + // return sceneFn(this.currentSceneData); + // } + // ) + // ); + } + + loadScene(sceneName: string, data: any) { + console.log(sceneName, data) + this.currentSceneData.set(data); + this.currentSceneName.set(sceneName); + } } diff --git a/packages/client/src/RpgClientEngine.ts b/packages/client/src/RpgClientEngine.ts index 75927207..cb399666 100644 --- a/packages/client/src/RpgClientEngine.ts +++ b/packages/client/src/RpgClientEngine.ts @@ -21,16 +21,20 @@ type FrameData = { } export class RpgClientEngine { + private options!: RpgClientEntryPointOptions - /** - * Get the rendering - * - * @prop {RpgRenderer} [renderer] - * @readonly - * @deprecated Use `inject(RpgRenderer)` instead. Will be removed in v5 - * @memberof RpgClientEngine - * */ - public renderer: RpgRenderer + /** + * Get the rendering + * + * @prop {RpgRenderer} [renderer] + * @readonly + * @memberof RpgClientEngine + * */ + private renderer: RpgRenderer; + private _serverUrl: string = ""; + private clientFrames: Map = new Map() + private serverFrames: Map = new Map() + private serverFps: number = 60 /** * Get the socket @@ -42,720 +46,267 @@ export class RpgClientEngine { public socket: any; io: any; - /** - * retrieve the global configurations assigned at the entry point - * - * @prop {object} [globalConfig] - * @readonly - * @memberof RpgClientEngine - * */ - public globalConfig: any = {} - - /** - * Get the class managing the keyboard - * - * @prop {KeyboardControls} [controls] - * @deprecated Use `inject(KeyboardControls)` instead. Will be removed in v5 - * @readonly - * @memberof RpgClientEngine - * */ - public controls: KeyboardControls - - public _options: any - - private _tick: BehaviorSubject = new BehaviorSubject({ - timestamp: -1, - deltaTime: 0, - frame: 0, - deltaRatio: 1 - }) - public keyChange: Subject = new Subject() - public roomJoin: Subject = new Subject() - private hasBeenDisconnected: boolean = false - private serverChanging: boolean = false - private isTeleported: boolean = false - // TODO, public or private - io - private lastTimestamp: number = 0 - private subscriptionWorld: Subscription - - private clientFrames: Map = new Map() - private serverFrames: Map = new Map() - - private session: string | null = null - private lastConnection: string = '' - private lastScene: string = '' - private matchMakerService: string | (() => MatchMakerResponse) | null = null - private serverFps: number = 60 - private scheduler: Scheduler = new Scheduler() - private _serverUrl: string = '' - /** - * * @deprecated Use `inject(GameEngineClient)` instead. Will be removed in v5 - */ - public gameEngine = this.context.inject(GameEngineClient) - - /** - * Read objects synchronized with the server - * - * @prop {Observable< { - [id: string]: { - object: any, - paramsChanged: any - } - } >} [objects] - * @readonly - * @memberof RpgClientEngine - */ - objects: Observable = this.gameEngine.objects - - envs?: object = {} - - constructor(private context: InjectContext, private options) { - this.envs = options.envs || {} - this.tick.subscribe(({ timestamp, deltaTime }) => { - if (timestamp != -1) this.step(timestamp, deltaTime) - }) - } - - private async _init() { - this.renderer = this.context.inject(RpgRenderer) - - const pluginLoadResource = async (hookName: string, type: string) => { - const resource = this.options[type] || [] - this.options[type] = [ - ...Utils.arrayFlat(await RpgPlugin.emit(hookName, resource)) || [], - ...resource - ] - } - - await pluginLoadResource(HookClient.AddSpriteSheet, 'spritesheets') - await pluginLoadResource(HookClient.AddGui, 'gui') - await pluginLoadResource(HookClient.AddSound, 'sounds') - - this.renderer.options = { - selector: '#rpg', - selectorCanvas: '#canvas', - selectorGui: '#gui', - canvas: {}, - gui: [], - spritesheets: [], - sounds: [], - ...this.options - } - - this.io = this.options.io - if (this.options.serverFps) this.serverFps = this.options.serverFps - this.globalConfig = this.options.globalConfig - this.gameEngine.standalone = this.options.standalone - this.gameEngine.renderer = this.renderer - this.gameEngine.clientEngine = this - - this.addSpriteSheet(this.renderer.options.spritesheets); - - (this.renderer.options.sounds || []).forEach(sound => { - const id: any = isString(sound) ? extractId(sound) : undefined - this.addSound(sound, id) - }) - - // deprecated - if (typeof __RPGJS_PRODUCTION__ != 'undefined' && __RPGJS_PRODUCTION__) { - if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/service-worker.js') - }) - } - } - - this.controls = this.context.inject(KeyboardControls) - } - - private addResource(resourceClass, cb) { - let array = resourceClass - if (!Utils.isArray(resourceClass)) { - array = [resourceClass] - } - cb(array, this) - } + /** + * retrieve the global configurations assigned at the entry point + * + * @prop {object} [globalConfig] + * @readonly + * @memberof RpgClientEngine + * */ + public globalConfig: any = {}; - /** - * Listen to each frame - * - * @prop {Observable<{ timestamp: number, deltaTime: number, frame: number }>} tick - * @readonly - * @since 3.0.0-beta.5 - * @memberof RpgClientEngine - * @example - * - * ```ts - * client.tick.subscribe(({ timestamp, deltaTime, frame }) => { - * - * }) - * ``` - * */ - get tick(): Observable { - return this.scheduler.tick as any - } + private gameEngine: GameEngineClient; - /** - * Adds Spritesheet classes - * - * @title Add Spritesheet - * @method addSpriteSheet(spritesheetClass|spritesheetClass[]) - * @param { Class|Class[] } spritesheetClass - * @method addSpriteSheet(url,id) - * @param {string} url Define the url of the resource - * @param {string} id Define a resource identifier - * @returns {Class} - * @since 3.0.0-beta.3 - * @memberof RpgClientEngine - */ - addSpriteSheet(spritesheetClass: constructor) - addSpriteSheet(url: string, id: string) - addSpriteSheet(spritesheetClass: constructor | string, id?: string): constructor { - if (typeof spritesheetClass === 'string') { - if (!id) { - throw log('Please, specify the resource ID (second parameter)') - } - @Spritesheet({ - id, - image: this.getResourceUrl(spritesheetClass) - }) - class AutoSpritesheet { } - spritesheetClass = AutoSpritesheet as any - } - this.addResource(spritesheetClass, _initSpritesheet) - return spritesheetClass as any - } + constructor(private context: Context) { + this.options = inject(context, ConfigToken) + this.gameEngine = inject(context, GameEngineClient); + this.renderer = inject(context, RpgRenderer); + } - /** - * Adds Sound classes - * - * @title Add Sound - * @method addSound(soundClass|soundClass[]) - * @param { Class|Class[] } soundClass - * @method addSound(url,id) - * @param {string} url Define the url of the resource - * @param {string} id Define a resource identifier - * @returns {Class} - * @since 3.0.0-beta.3 - * @memberof RpgClientEngine - */ - addSound(soundClass: constructor) - addSound(url: string, id: string) - addSound(soundClass: constructor | string, id?: string): constructor { - if (typeof soundClass === 'string') { - if (!id) { - throw log('Please, specify the resource ID (second parameter)') - } - @Sound({ - id, - sound: this.getResourceUrl(soundClass) - }) - class AutoSound { } - soundClass = AutoSound as any - } - this.addResource(soundClass, _initSound) - return soundClass as any + /** + * Starts the client side and connects to the server + * + * @title Start Client Engine + * @method start() + * @returns {Promise< RpgClientEngine >} + * @memberof RpgClientEngine + */ + async start( + options: { renderLoop: boolean } = { + renderLoop: true, } + ): Promise { + + this.io = this.options.io; - getResourceUrl(source: string): string { - // @ts-ignore - if (window.urlCache && window.urlCache[source]) { - // @ts-ignore - return window.urlCache[source] - } + const pluginLoadResource = async (hookName: string, type: string) => { + const resource = this.options[type] || []; + this.options[type] = [ + ...(Utils.arrayFlat(await RpgPlugin.emit(hookName, resource)) || []), + ...resource, + ]; + }; - if (source.startsWith('data:')) { - return source - } + await pluginLoadResource(HookClient.AddSpriteSheet, "spritesheets"); - // @ts-ignore - const staticDir = this.envs.VITE_BUILT + this.addSpriteSheet(this.options.spritesheets); - if (staticDir) { - return this.assetsPath + '/' + Utils.basename(source) - } + await this.renderer.init(); + const { maxFps } = this.options - return source + if (options.renderLoop) { + setInterval(() => { + this.processInput() + }, Utils.fps2ms(this.serverFps)) } - /** - * Starts the client side and connects to the server - * - * @title Start Client Engine - * @method start() - * @returns {Promise< RpgClientEngine >} - * @memberof RpgClientEngine - */ - async start(options: { renderLoop: boolean } = { - renderLoop: true - }): Promise { - await this._init() - await this.renderer.init() - const { maxFps } = this.options - - if (options.renderLoop) { - this.scheduler.start({ - maxFps - }) - // The processing is outside the rendering loop because if the FPS are lower (or higher) then the sending to the server would be slower or faster. Here it is constant - setInterval(() => { - this.processInput() - }, Utils.fps2ms(this.serverFps)) - } - const ret: boolean[] = await RpgPlugin.emit(HookClient.Start, this) - this.matchMakerService = this.options.globalConfig.matchMakerService - const hasFalseValue = ret.findIndex(el => el === false) != - 1 - if (!hasFalseValue) { - let serverUri = {} as MatchMakerResponse - if (this.matchMakerService) { - if (Utils.isFunction(this.matchMakerService)) { - serverUri = (this.matchMakerService as Function)() - } - else { - serverUri = await lastValueFrom(ajax.getJSON(this.matchMakerService as string)) - } + // @ts-ignore + const envUrl = this.envs.VITE_SERVER_URL; + console.log(envUrl) + let serverUri = {} as MatchMakerResponse; + await this.connection( + serverUri.url + ? serverUri.url + ":" + serverUri.port + : envUrl + ? envUrl + : undefined + ); + return this; + } - } - // @ts-ignore - const envUrl = this.envs.VITE_SERVER_URL - await this.connection( - serverUri.url ? serverUri.url + ':' + serverUri.port : - envUrl ? envUrl : undefined - ) - } - return this + getResourceUrl(source: string): string { + // @ts-ignore + if (window.urlCache && window.urlCache[source]) { + // @ts-ignore + return window.urlCache[source]; } - /** - * Display the next frame. Useful for unit tests - * - * @title Next Frame - * @since 3.0.0-beta.5 - * @param {number} timestamp Indicate the timestamp of the frame - * @method nextFrame() - * @memberof RpgClientEngine - */ - nextFrame(timestamp: number): void { - this.scheduler.nextTick(timestamp) + if (source.startsWith("data:")) { + return source; } - async sendInput(actionName: string | Control) { - const player = this.player - if (!player) return - if (player.canMove) { - player.pendingMove.push({ - input: actionName, - frame: this.scheduler.frame - }) - } - } + // @ts-ignore + const staticDir = this.envs.VITE_BUILT; - get player(): RpgCommonPlayer | null { - return this.gameEngine.world.getObject(this.gameEngine.playerId) + if (staticDir) { + return this.assetsPath + "/" + Utils.basename(source); } - private serverReconciliation(player: RpgCommonPlayer) { - let garbage: number[] = [] - this.serverFrames.forEach((serverData, frame) => { - const { data: serverPos, time: serverTime } = serverData - const client = this.clientFrames.get(frame) - if (!client || (client && client.data.x != serverPos.x || client.data.y != serverPos.y)) { - if (serverPos.x) player.position.x = serverPos.x - if (serverPos.y) player.position.y = serverPos.y - } - player.position.z = serverPos.z - garbage.push(frame) - }) - garbage.forEach(frame => { - this.serverFrames.delete(frame) - this.clientFrames.delete(frame) - }) - garbage = [] - } + return source; + } - private async step(t: number, dt: number) { - RpgPlugin.emit(HookClient.Step, [this, t, dt], true) + /** + * Adds Spritesheet classes + * + * @title Add Spritesheet + * @method addSpriteSheet(spritesheetClass|spritesheetClass[]) + * @param { Class|Class[] } spritesheetClass + * @method addSpriteSheet(url,id) + * @param {string} url Define the url of the resource + * @param {string} id Define a resource identifier + * @returns {Class} + * @since 3.0.0-beta.3 + * @memberof RpgClientEngine + */ + addSpriteSheet(spritesheetClass: constructor); + addSpriteSheet(url: string, id: string); + addSpriteSheet( + spritesheetClass: constructor | string, + id?: string + ): constructor { + if (typeof spritesheetClass === "string") { + if (!id) { + throw log("Please, specify the resource ID (second parameter)"); + } + @Spritesheet({ + id, + image: this.getResourceUrl(spritesheetClass), + }) + class AutoSpritesheet {} + spritesheetClass = AutoSpritesheet as any; } + this.addResource(spritesheetClass, _initSpritesheet); + return spritesheetClass as any; + } - async processInput() { - const player = this.player - this.controls.preStep() - if (player) { - if (player.pendingMove.length > 0) { - const { inputs: inputEvent } = await this.gameEngine.processInput(this.gameEngine.playerId, this.controls.options) - if (inputEvent.length == 0) return - const frame = Date.now() - this.clientFrames.set(frame, { - data: player.position.copy(), - time: frame - }) - if (this.socket) { - this.socket.emit('move', { input: inputEvent, frame }) - } - RpgPlugin.emit(HookClient.SendInput, [this, inputEvent], true) - } - if (player.canMove) this.serverReconciliation(player) - } + private addResource(resourceClass, cb) { + let array = resourceClass; + if (!Utils.isArray(resourceClass)) { + array = [resourceClass]; } + cb(array, this); + } - /** - *Connect to the server - * - * @title Connect to server - * @method connection() - * @returns {void} - * @memberof RpgClientEngine - */ - async connection(uri?: string) { - const { standalone } = this.gameEngine - const { globalConfig } = this - - this._serverUrl = uri || '' - - if (!standalone) { - this.socket = this.io(uri, { - auth: { - token: this.session - }, - ...(globalConfig.socketIoClient || {}) - }) - } - else { - this.socket = this.io + /** + *Connect to the server + * + * @title Connect to server + * @method connection() + * @returns {void} + * @memberof RpgClientEngine + */ + async connection(uri?: string) { + const { globalConfig } = this; + this.socket = this.io(uri, { + auth: { + token: this.gameEngine.session(), + }, + ...(globalConfig.socketIoClient || {}), + }); + + this.socket.on(SocketEvents.LoadScene, ({ name, data }) => { + this.renderer.loadScene(name, data); + }); + + this.socket.on("playerJoined", (playerEvent) => { + this.gameEngine.playerId.set(playerEvent.playerId); + this.gameEngine.session.set(playerEvent.session); + }); + + this.world.listen(this.socket).value.subscribe( + async (val: { + data: any; + partial: any; + time: number; + roomId: string; + resetProps: string[]; + }) => { + if (!val.data) { + return; } - - this.socket.on('connect', () => { - if (RpgGui.exists(PrebuiltGui.Disconnect)) RpgGui.hide(PrebuiltGui.Disconnect) - RpgPlugin.emit(HookClient.Connected, [this, this.socket], true) - this.hasBeenDisconnected = false - }) - - this.socket.on('playerJoined', (playerEvent) => { - this.gameEngine.playerId = playerEvent.playerId - this.session = playerEvent.session - }) - - this.socket.on('connect_error', (err: any) => { - RpgPlugin.emit(HookClient.ConnectedError, [this, err, this.socket], true) - }) - - this.socket.on('preLoadScene', ({ id, reconnect }: { id: string, reconnect?: boolean }) => { - if (this.lastScene == id) { - return - } - this.lastScene = id - this.renderer.transitionScene(id) - if (reconnect) { - this.roomJoin.next('') - this.roomJoin.complete() + const change = (prop, root = val, localEvent = false) => { + const list = root.data[prop]; + const partial = root.partial[prop]; + const isShape = prop == "shapes"; + if (!partial) { + return; + } + if (val.resetProps.indexOf(prop) != -1) { + // todo + } + for (let key in partial) { + const obj = list[key]; + const paramsChanged = partial ? partial[key] : undefined; + + if (obj == null || obj.deleted) { + // todo + continue; } - }) - this.socket.on(SocketEvents.GameReload, () => { - window.location.reload() - }) + if (!obj) continue; + + this.gameEngine.updateObject({ + playerId: key, + params: obj, + localEvent, + paramsChanged, + isShape, + }); + } + }; + change("users"); + change("events"); + change("shapes"); + } + ); + } - this.socket.on(SocketEvents.LoadScene, ({ name, data }) => { - this.renderer.loadScene(name, data) - }) + get world(): any { + return World; + } - this.socket.on(SocketEvents.ChangeServer, async ({ url, port }) => { - const connection = url + ':' + port - if (this.lastConnection == connection) { - return - } - if (this.subscriptionWorld) { - this.subscriptionWorld.unsubscribe() - } - this.lastConnection = connection - this.serverChanging = true - this.socket.disconnect() - this.connection(connection) - }) - - this.socket.on('changeTile', ({ tiles, x, y }) => { - const scene = this.renderer.getScene() - scene?.changeTile(x, y, tiles) - }) - - const callMethod = ({ objectId, params, name }) => { - const scene = this.renderer.getScene() - const sprite = scene?.getPlayer(objectId) - if (!sprite) return - switch (name) { - case SocketMethods.ShowAnimation: - scene?.showAnimation({ - attachTo: sprite, - graphic: params[0], - animationName: params[1], - replaceGraphic: params[2] - }) - break - case SocketMethods.CameraFollow: - const [spriteId, options] = params - scene?.cameraFollowSprite(spriteId, options) - break - case SocketMethods.PlaySound: - RpgSound.play(params[0]) - break - case SocketMethods.ModeMove: - const player = this.player - const { checkCollision } = params[0] - if (player) { - player.checkCollision = checkCollision - } - break - } - } + /** + * get player id of the current player + * @prop {string} [playerId] + * @readonly + * @memberof RpgClientEngine + */ + get playerId(): string { + return this.gameEngine.playerId(); + } - this.socket.on(SocketEvents.CallMethod, callMethod) - - let lastRoomId = '' - - this.subscriptionWorld = World.listen(this.socket) - .value - .subscribe(async (val: { data: any, partial: any, time: number, roomId: string, resetProps: string[] }) => { - const scene = this.renderer.getScene() - - if (!val.data) { - return - } - - const partialRoom = val.partial - - if (val.roomId != lastRoomId) { - this.clientFrames.clear() - this.serverFrames.clear() - this.gameEngine.resetObjects() - lastRoomId = val.roomId - this.isTeleported = false - } - - const objectsChanged = {} - - const callAction = (objectId: string, paramsChanged) => { - if (paramsChanged && SocketEvents.CallMethod in paramsChanged) { - // Force rendering on the map (display events) and then perform actions on it (animation, etc.). - this.renderer.draw(Date.now(), 1, 1, 1) - callMethod({ - objectId, - ...paramsChanged[SocketEvents.CallMethod] - }) - } - } - - const change = (prop, root = val, localEvent = false) => { - const list = root.data[prop] - const partial = root.partial[prop] - const isShape = prop == 'shapes' - if (!partial) { - return - } - if (val.resetProps.indexOf(prop) != -1) { - const objects = isShape ? this.gameEngine.getShapes() : this.gameEngine.getObjects() - for (let key in objects) { - const obj = objects[key] - if (obj) { - this.gameEngine.removeObjectAndShape(key) - } - } - } - for (let key in partial) { - const obj = list[key] - const paramsChanged = partial ? partial[key] : undefined - - if (obj == null || obj.deleted) { - // perform actions on the sprite before deleting it - callAction(key, paramsChanged) - this.gameEngine.removeObjectAndShape(key) - continue - } - - if (!obj) continue - - if (!isShape) { - obj.type = { - users: PlayerType.Player, - events: PlayerType.Event - }[prop] - } - if (prop == 'users' && this.gameEngine.playerId == key) { - if (obj.events) { - const nbEvents = Object.values(obj.events) - if (nbEvents.length == 0) { - this.gameEngine.events = {} - } - else { - change('events', { - data: obj, - partial: paramsChanged, - time: val.time, - roomId: val.roomId, - resetProps: val.resetProps - }, true) - } - } - if (partialRoom?.pos && partialRoom?.frame !== undefined) { - this.serverFrames.set(partialRoom.frame, { - data: partialRoom.pos, - time: Date.now() - }) - } - } - objectsChanged[key] = this.gameEngine.updateObject({ - playerId: key, - params: obj, - localEvent, - paramsChanged, - isShape - }) - - // perform actions on the sprite after creation/update - callAction(key, paramsChanged) - } - } - - if (partialRoom.join) { - this.roomJoin.next(partialRoom) - this.roomJoin.complete() - } - - change('users') - change('events') - change('shapes') - - this.gameEngine.setObjectsChanged(objectsChanged) - - if (scene) { - scene.update(val) - } - }) + get player(): RpgCommonPlayer | null { + return this.gameEngine.player() + } - this.socket.on('disconnect', (reason: string) => { - if (this.serverChanging) { - return - } - if (RpgGui.exists(PrebuiltGui.Disconnect)) RpgGui.display(PrebuiltGui.Disconnect) - RpgPlugin.emit(HookClient.Disconnect, [this, reason, this.socket], true) - this.hasBeenDisconnected = true - }) - - RpgGui._setSocket(this.socket) - - if (standalone) { - await this.socket.connection({ - auth: { - token: this.session - } + async processInput() { + const player = this.player + /*if (player) { + if (player.pendingMove.length > 0) { + const { inputs: inputEvent } = await this.gameEngine.processInput(this.playerId) + if (inputEvent.length == 0) return + const frame = Date.now() + this.clientFrames.set(frame, { + data: player.position.copy(), + time: frame }) + if (this.socket) { + this.socket.emit('move', { input: inputEvent, frame }) + } + RpgPlugin.emit(HookClient.SendInput, [this, inputEvent], true) } + // this.serverReconciliation(player) + }*/ +} - this.serverChanging = false - } - - get world(): any { - return World - } - - // shortcuts - - /** - * VueJS Application instance - * - * [https://v3.vuejs.org/api/application-api.html](https://v3.vuejs.org/api/application-api.html) - * - * @prop {Vue} [vueApp] - * @readonly - * @memberof RpgClientEngine - * */ - get vueApp() { - return this.renderer.app - } - - /** - * VueJS Parent component instance - * - * [https://v3.vuejs.org/api/instance-properties.html](https://v3.vuejs.org/api/instance-properties.html) - * - * @prop {Vue Instance} [vueInstance] - * @readonly - * @memberof RpgClientEngine - * */ - get vueInstance() { - return this.renderer.vm - } - - /** - * retrieves the current scene (SceneMap if you are on a map) - * - * @prop {RpgScene} [scene] - * @deprecated - * @readonly - * @memberof RpgClientEngine - * */ - get scene() { - return this.renderer.getScene() - } - - /** - * retrieves the current scene (SceneMap if you are on a map) - * - * @title Connect to server - * @method getScene() - * @returns {RpgScene} - * @memberof RpgClientEngine - */ - getScene(): T | null { - return this.renderer.getScene() - } - - /** - * get PIXI class - * @prop {PIXI} [PIXI] - * @readonly - * @memberof RpgClientEngine - */ - get PIXI() { - return PIXI - } - - /** - * get player id of the current player - * @prop {string} [playerId] - * @readonly - * @memberof RpgClientEngine - */ - get playerId(): string { - return this.gameEngine.playerId - } - - /** - * Finds the game mode from the environment variables sent by the compiler. - * Can be used in menus to display options according to type - * - * @title Game Type - * @prop {string|undefined} [gameType] mmorpg | rpg or undefined if environment variable not found - * @readonly - * @memberof RpgClientEngine - * @since 4.0.0 - */ - get gameType(): 'mmorpg' | 'rpg' | undefined { - return this.envs?.['VITE_RPG_TYPE'] - } - - /** - * Find out if the game is in production or not, from the environment variables sent by the compiler. - * - * @title Game is dev mode - * @prop {boolean} [isDev] - * @readonly - * @memberof RpgClientEngine - * @since 4.0.0 - */ - get isDev(): boolean { - return !this.envs?.['VITE_BUILT'] - } + private serverReconciliation(player: RpgCommonPlayer) { + let garbage: number[] = [] + this.serverFrames.forEach((serverData, frame) => { + const { data: serverPos, time: serverTime } = serverData + const client = this.clientFrames.get(frame) + if (!client || (client && client.data.x != serverPos.x || client.data.y != serverPos.y)) { + if (serverPos.x) player.position.x = serverPos.x + if (serverPos.y) player.position.y = serverPos.y + } + player.position.z = serverPos.z + garbage.push(frame) + }) + garbage.forEach(frame => { + this.serverFrames.delete(frame) + this.clientFrames.delete(frame) + }) + garbage = [] + } /** * Get the server url. This is the url for the websocket @@ -775,29 +326,11 @@ export class RpgClientEngine { return this._serverUrl; } - get assetsPath(): string { - return this.envs?.['VITE_ASSETS_PATH'] || 'assets' - } - - get module() { - return RpgPlugin - } + get assetsPath(): string { + return this.envs?.["VITE_ASSETS_PATH"] || "assets"; + } - reset() { - this.subscriptionWorld.unsubscribe() - this.world.reset() - spritesheets.clear() - sounds.clear() - Assets.reset() - utils.clearTextureCache() - for (let textureUrl in utils.BaseTextureCache) { - delete utils.BaseTextureCache[textureUrl] - } - for (let textureUrl in utils.TextureCache) { - delete utils.TextureCache[textureUrl] - } - RpgGui.clear() - RpgCommonMap.bufferClient.clear() - RpgSound.clear() - } + get envs(): any { + return this.options.envs + } }