From bef3c020ae5ff8cc50e9221ba5b8a097c8c9ebe8 Mon Sep 17 00:00:00 2001 From: RSamaium Date: Tue, 9 Apr 2024 16:35:27 +0200 Subject: [PATCH] sprite component --- packages/client/src/Components/Character.ts | 59 +++++++ packages/client/src/GameEngine.ts | 52 ++++++ .../src/Presets/AnimationSpritesheet.ts | 93 +++++----- packages/client/src/Resources.ts | 57 ++++++ packages/client/src/RpgClientEngine.ts | 167 ++++++++++++++++-- packages/client/src/Scenes/Map.ts | 12 +- packages/client/src/decorators/Spritesheet.ts | 6 +- .../src/build/vite-plugin-config.toml.ts | 6 +- packages/sample2/main/player.ts | 15 +- .../main/spritesheets/npc/spritesheet.ts | 10 +- packages/sample2/main/worlds/maps/map.tmx | 2 +- 11 files changed, 394 insertions(+), 85 deletions(-) create mode 100644 packages/client/src/Components/Character.ts create mode 100644 packages/client/src/GameEngine.ts create mode 100644 packages/client/src/Resources.ts diff --git a/packages/client/src/Components/Character.ts b/packages/client/src/Components/Character.ts new file mode 100644 index 00000000..c4164c7f --- /dev/null +++ b/packages/client/src/Components/Character.ts @@ -0,0 +1,59 @@ +import { Container, Sprite, signal } from "canvasengine"; +import { GameEngineClient } from "../GameEngine"; +import { spritesheets } from "../Resources"; +import { inject } from "../inject"; + +export function CharacterComponent(props) { + const graphic = props.layout?.center.lines[0].col[0].value; + const { direction, x, y } = props; + const game = inject(GameEngineClient); + if (!graphic) { + return Container(); + } + + const controls = signal({ + down: { + repeat: true, + bind: "down", + trigger() { + y.update((y) => y + 3); + }, + }, + up: { + repeat: true, + bind: "up", + trigger() { + y.update((y) => y - 3); + }, + }, + left: { + repeat: true, + bind: "left", + trigger() { + x.update((x) => x - 3); + }, + }, + right: { + repeat: true, + bind: "right", + trigger() { + x.update((x) => x + 3); + }, + }, + }); + + const spritesheet = spritesheets.get(graphic); + + return Sprite({ + sheet: { + definition: spritesheet.$decorator, + playing: "stand", + params: { + direction + } + }, + controls: props.id == game.playerId() ? controls : undefined, + x, + y, + }); +} diff --git a/packages/client/src/GameEngine.ts b/packages/client/src/GameEngine.ts new file mode 100644 index 00000000..4a50bebc --- /dev/null +++ b/packages/client/src/GameEngine.ts @@ -0,0 +1,52 @@ +import { signal } from "canvasengine"; + +export class GameEngineClient { + playerId = signal(""); + session = signal(""); + objects = signal([]); + + animationX: any; + animationY: any; + + updateObject(obj) { + const { playerId: id, params, localEvent, paramsChanged, isShape } = obj; + const findObject = this.objects().find((o: any) => o.id == id); + if (!findObject) { + this.objects.mutate((objs) => + objs.push({ + id, + ...params, + x: signal(params.position?.x ?? params.x), + y: signal(params.position?.y ?? params.y), + direction: signal(params.direction), + }) + ); + } else { + if (paramsChanged.position?.x) { + if (this.animationX) { + this.animationX.stop(); + } + this.animationX = findObject.x.animate( + paramsChanged.position?.x ?? params.x, + { + duration: 50, + } + ); + } + if (paramsChanged.position?.y) { + if (this.animationY) { + this.animationY.stop(); + } + this.animationY = findObject.y.animate( + paramsChanged.position?.y ?? params.y, + { + duration: 50, + } + ); + } + if (paramsChanged.direction !== undefined) { + findObject.direction.set(paramsChanged.direction); + } + } + } +} diff --git a/packages/client/src/Presets/AnimationSpritesheet.ts b/packages/client/src/Presets/AnimationSpritesheet.ts index 8afb421e..5452e230 100644 --- a/packages/client/src/Presets/AnimationSpritesheet.ts +++ b/packages/client/src/Presets/AnimationSpritesheet.ts @@ -1,50 +1,59 @@ -import { Direction } from '@rpgjs/common' +import { Direction } from "@rpgjs/common"; export enum Animation { - Stand = 'stand', - Walk = 'walk', - Attack = 'attack', - Defense = 'defense', - Skill = 'skill' + Stand = "stand", + Walk = "walk", + Attack = "attack", + Defense = "defense", + Skill = "skill", } -export const RMSpritesheet = (framesWidth: number, framesHeight: number, frameStand: number = 1) => { +export const RMSpritesheet = ( + framesWidth: number, + framesHeight: number, + frameStand: number = 1 +) => { + if (framesWidth <= frameStand) { + frameStand = framesWidth - 1; + } - if (framesWidth <= frameStand) { - frameStand = framesWidth - 1 - } - - const frameY = direction => { - const gap = Math.max(4 - framesHeight, 0) - return { - [Direction.Down]: 0, - [Direction.Left]: Math.max(0, 1 - gap), - [Direction.Right]: Math.max(0, 2 - gap), - [Direction.Up]: Math.max(0, 3 - gap) - }[direction] - } + const frameY = (direction) => { + const gap = Math.max(4 - framesHeight, 0); + return { + [Direction.Down]: 0, + [Direction.Left]: Math.max(0, 1 - gap), + [Direction.Right]: Math.max(0, 2 - gap), + [Direction.Up]: Math.max(0, 3 - gap), + }[direction]; + }; - const stand = (direction: number) => [{ time: 0, frameX: frameStand, frameY: frameY(direction) }] - const walk = direction => { - const array: any = [] - const durationFrame = 10 - for (let i = 0; i < framesWidth; i++) { - array.push({ time: i * durationFrame, frameX: i, frameY: frameY(direction) }) - } - array.push({ time: array[array.length - 1].time + durationFrame }) - return array + const stand = (direction: number) => [ + { time: 0, frameX: frameStand, frameY: frameY(direction) }, + ]; + const walk = (direction) => { + const array: any = []; + const durationFrame = 10; + for (let i = 0; i < framesWidth; i++) { + array.push({ + time: i * durationFrame, + frameX: i, + frameY: frameY(direction), + }); } + array.push({ time: array[array.length - 1].time + durationFrame }); + return array; + }; - return { - textures: { - [Animation.Stand]: { - animations: direction => [stand(direction)] - }, - [Animation.Walk]: { - animations: direction => [walk(direction)] - } - }, - framesHeight, - framesWidth - } -} \ No newline at end of file + return { + textures: { + [Animation.Stand]: { + animations: ({ direction }) => [stand(direction)], + }, + [Animation.Walk]: { + animations: ({ direction }) => [walk(direction)], + }, + }, + framesHeight, + framesWidth, + }; +}; diff --git a/packages/client/src/Resources.ts b/packages/client/src/Resources.ts new file mode 100644 index 00000000..5e567978 --- /dev/null +++ b/packages/client/src/Resources.ts @@ -0,0 +1,57 @@ +import { RpgClientEngine } from "./RpgClientEngine"; +/** +* Get/Set images in resources + ```ts + import { RpgResource } from '@rpg/client' + const fileLink = RpgResource.spritesheets.get('resource_id') + ``` +* @title Get/Set image link +* @prop { Map< string, string > } spritesheets +* @memberof Resources +*/ +/** +* Get/Set sounds in resources + ```ts + import { RpgResource } from '@rpg/client' + const fileLink = RpgResource.sounds.get('resource_id') + ``` +* @title Get/Set sound link +* @prop { Map< string, string > } sounds +* @memberof Resources +*/ +export async function _initResource( + memory: Map, + _resources, + prop: string, + engine: RpgClientEngine +) { + for (let resource of _resources) { + const pluralProp = prop + "s"; + const propDecorator = resource.$decorator[pluralProp] + if (propDecorator && !resource.$decorator[prop]) { + for (let key in propDecorator) { + const instance = new resource(); + instance.$decorator = resource.$decorator; + instance.$decorator[prop] = engine.getResourceUrl(propDecorator[key]); + delete instance.$decorator[pluralProp]; + instance.id = instance.$decorator.id = key; + memory.set(key, instance); + } + } else { + const instance = new resource(engine); + const id = resource.$decorator.id + if (!id) { + throw new Error(`Resource ${resource[prop]} must have an id`); + } + instance.$decorator = resource.$decorator; + instance.$decorator[prop] = engine.getResourceUrl(instance.$decorator[prop]); + memory.set(id, instance); + } + } +} + +export const spritesheets: Map = new Map(); + +export function _initSpritesheet(_spritesheets, engine: RpgClientEngine) { + return _initResource(spritesheets, _spritesheets, "image", engine); +} diff --git a/packages/client/src/RpgClientEngine.ts b/packages/client/src/RpgClientEngine.ts index 56c81690..d34cce86 100644 --- a/packages/client/src/RpgClientEngine.ts +++ b/packages/client/src/RpgClientEngine.ts @@ -1,7 +1,11 @@ -import { InjectContext } from "@rpgjs/common"; -import { SocketEvents } from "@rpgjs/types"; -import { World } from 'simple-room-client'; +import { HookClient, InjectContext, RpgPlugin, Utils } from "@rpgjs/common"; +import { log } from "@rpgjs/common/lib/Logger"; +import { SocketEvents, constructor } from "@rpgjs/types"; +import { World } from "simple-room-client"; +import { GameEngineClient } from "./GameEngine"; import { RpgRenderer } from "./Renderer"; +import { _initSpritesheet } from "./Resources"; +import { Spritesheet } from "./decorators/Spritesheet"; type MatchMakerResponse = { url: string; @@ -14,10 +18,10 @@ export class RpgClientEngine { * * @prop {RpgRenderer} [renderer] * @readonly - * @deprecated Use `inject(RpgRenderer)` instead. Will be removed in v5 * @memberof RpgClientEngine * */ - public renderer: RpgRenderer; + private renderer: RpgRenderer; + private _serverUrl: string = ""; /** * Get the socket @@ -38,6 +42,8 @@ export class RpgClientEngine { * */ public globalConfig: any = {}; + private gameEngine = this.context.inject(GameEngineClient); + envs?: object = {}; constructor(private context: InjectContext, private scenes, private options) { @@ -58,6 +64,18 @@ export class RpgClientEngine { }, ]); this.io = this.options.io; + + 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"); + + this.addSpriteSheet(this.options.spritesheets); } /** @@ -88,6 +106,73 @@ export class RpgClientEngine { return this; } + getResourceUrl(source: string): string { + // @ts-ignore + if (window.urlCache && window.urlCache[source]) { + // @ts-ignore + return window.urlCache[source]; + } + + if (source.startsWith("data:")) { + return source; + } + + // @ts-ignore + const staticDir = this.envs.VITE_BUILT; + + if (staticDir) { + return this.assetsPath + "/" + Utils.basename(source); + } + + return source; + } + + /** + * 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; + } + + private addResource(resourceClass, cb) { + let array = resourceClass; + if (!Utils.isArray(resourceClass)) { + array = [resourceClass]; + } + cb(array, this); + } + + async processInput() { + + } + /** *Connect to the server * @@ -100,7 +185,7 @@ export class RpgClientEngine { const { globalConfig } = this; this.socket = this.io(uri, { auth: { - // token: this.session, + token: this.gameEngine.session(), }, ...(globalConfig.socketIoClient || {}), }); @@ -109,7 +194,12 @@ export class RpgClientEngine { this.renderer.loadScene(name, data); }); - World.listen(this.socket).value.subscribe( + this.socket.on("playerJoined", (playerEvent) => { + this.gameEngine.playerId.set(playerEvent.playerId); + this.gameEngine.session.set(playerEvent.session); + }); + + World.listen(this.socket).value.subscribe( async (val: { data: any; partial: any; @@ -117,15 +207,9 @@ export class RpgClientEngine { roomId: string; resetProps: string[]; }) => { - if (!val.data) { return; } - - const partialRoom = val.partial; - - const objectsChanged = {}; - const change = (prop, root = val, localEvent = false) => { const list = root.data[prop]; const partial = root.partial[prop]; @@ -133,20 +217,69 @@ export class RpgClientEngine { 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; - console.log(obj) - - } - }; + if (obj == null || obj.deleted) { + // todo + continue; + } + if (!obj) continue; + this.gameEngine.updateObject({ + playerId: key, + params: obj, + localEvent, + paramsChanged, + isShape, + }); + } + }; change("users"); change("events"); change("shapes"); } ); } + + get world(): any { + return World; + } + + /** + * get player id of the current player + * @prop {string} [playerId] + * @readonly + * @memberof RpgClientEngine + */ + get playerId(): string { + return this.gameEngine.playerId(); + } + + /** + * Get the server url. This is the url for the websocket + * + * To customize the URL, use the `matchMakerService` configuration + * + * @title Server URL + * @prop {string} [serverUrl] If empty string, server url is same as client url + * @readonly + * @memberof RpgClientEngine + * @since 4.0.0 + */ + get serverUrl(): string { + if (!this._serverUrl.startsWith("http")) { + return "http://" + this._serverUrl; + } + return this._serverUrl; + } + + get assetsPath(): string { + return this.envs?.["VITE_ASSETS_PATH"] || "assets"; + } } diff --git a/packages/client/src/Scenes/Map.ts b/packages/client/src/Scenes/Map.ts index 83e3417c..83791fa2 100644 --- a/packages/client/src/Scenes/Map.ts +++ b/packages/client/src/Scenes/Map.ts @@ -1,11 +1,15 @@ import { TiledMap as Map } from "@rpgjs/tiled"; -import { Signal, TiledMap, h } from "canvasengine"; +import { Signal, TiledMap, h, loop } from "canvasengine"; +import { CharacterComponent } from "../Components/Character"; +import { GameEngineClient } from "../GameEngine"; import { RpgRenderer } from "../Renderer"; import { inject } from "../inject"; export function SceneMap(props: Signal) { const { height, tileheight, width, tilewidth } = props(); const renderer = inject(RpgRenderer); + const game = inject(GameEngineClient); + // TODO: Waiting Viewport to be implemented. /*return h( @@ -23,6 +27,10 @@ export function SceneMap(props: Signal) { );*/ return h(TiledMap, { map: props, - objectLayer: (layer) => {}, + objectLayer: (layer) => { + return loop(game.objects, (object, index) => { + return h(CharacterComponent, object) + }) + }, }); } diff --git a/packages/client/src/decorators/Spritesheet.ts b/packages/client/src/decorators/Spritesheet.ts index 039ca5c8..43cc2829 100644 --- a/packages/client/src/decorators/Spritesheet.ts +++ b/packages/client/src/decorators/Spritesheet.ts @@ -379,11 +379,7 @@ export function Spritesheet(options: SpritesheetImageOptions) export function Spritesheet(options: SpritesheetImagesOptions) export function Spritesheet(options: SpritesheetImageOptions | SpritesheetImagesOptions) { return (target: Function) => { - if ('images' in options) target['images'] = options.images - if ('id' in options) target['id'] = options.id - for (let key in options) { - target.prototype[key] = options[key] - } + target['$decorator'] = options return } } \ No newline at end of file diff --git a/packages/compiler/src/build/vite-plugin-config.toml.ts b/packages/compiler/src/build/vite-plugin-config.toml.ts index 89ebd1e4..f294c219 100644 --- a/packages/compiler/src/build/vite-plugin-config.toml.ts +++ b/packages/compiler/src/build/vite-plugin-config.toml.ts @@ -222,11 +222,11 @@ export function loadSpriteSheet(directoryName: string, modulePath: string, optio else { const dimensions = sizeOf(lastImagePath) propImagesString = ` - ${importSprites?.variablesString}.images = { + ${importSprites?.variablesString}.$decorator.images = { ${objectString} } - ${importSprites?.variablesString}.prototype.width = ${dimensions.width} - ${importSprites?.variablesString}.prototype.height = ${dimensions.height} + ${importSprites?.variablesString}.$decorator.width = ${dimensions.width} + ${importSprites?.variablesString}.$decorator.height = ${dimensions.height} ` } } diff --git a/packages/sample2/main/player.ts b/packages/sample2/main/player.ts index 3b5d3524..3ecdf729 100644 --- a/packages/sample2/main/player.ts +++ b/packages/sample2/main/player.ts @@ -1,12 +1,10 @@ -import { RpgMap } from '@rpgjs/server'; -import { Speed } from '@rpgjs/server'; -import { RpgPlayer, RpgPlayerHooks, Control, Components, RpgEvent, EventData } from '@rpgjs/server' -import Potion from './database/items/Potion'; +import { Components, Move, RpgPlayer, RpgPlayerHooks } from '@rpgjs/server'; const player: RpgPlayerHooks = { onConnected(player: RpgPlayer) { player.name = 'YourName' player.setComponentsTop(Components.text('{position.x},{position.y}')) + }, onInput(player: RpgPlayer, { input }) { const map = player.getCurrentMap() @@ -23,12 +21,11 @@ const player: RpgPlayerHooks = { player.callMainMenu() } }, - async onJoinMap(player: RpgPlayer) { + async onJoinMap(player: RpgPlayer, map) { player.gui('test').open(); - - setTimeout(() => { - player.addItem(Potion, 1); - }, 5000); + const event = map.getEventByName('EV-1') as any + event.speed = 1 + event.infiniteMoveRoute([Move.tileRandom()]) } } diff --git a/packages/sample2/main/spritesheets/npc/spritesheet.ts b/packages/sample2/main/spritesheets/npc/spritesheet.ts index 5b897074..87a55eca 100644 --- a/packages/sample2/main/spritesheets/npc/spritesheet.ts +++ b/packages/sample2/main/spritesheets/npc/spritesheet.ts @@ -1,8 +1,6 @@ -import { Spritesheet, Presets } from '@rpgjs/client' +import { Presets, Spritesheet } from "@rpgjs/client"; -const { RMSpritesheet } = Presets +const { RMSpritesheet } = Presets; -@Spritesheet( - RMSpritesheet(3, 4) -) -export default class Characters { } \ No newline at end of file +@Spritesheet(RMSpritesheet(3, 4)) +export default class Characters {} diff --git a/packages/sample2/main/worlds/maps/map.tmx b/packages/sample2/main/worlds/maps/map.tmx index 38f8e149..4a73cff4 100644 --- a/packages/sample2/main/worlds/maps/map.tmx +++ b/packages/sample2/main/worlds/maps/map.tmx @@ -15,7 +15,7 @@ - +