@rpgjs/client 4.0.2 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/browser/React-e57feed9.js +31136 -0
- package/browser/manifest.json +5 -0
- package/browser/rpg.client.js +435 -362
- package/browser/rpg.client.umd.cjs +31582 -373
- package/lib/Components/Component.js +6 -0
- package/lib/Components/Component.js.map +1 -1
- package/lib/GameEngine.d.ts +1 -0
- package/lib/GameEngine.js +6 -2
- package/lib/GameEngine.js.map +1 -1
- package/lib/{RpgGui.d.ts → Gui/Gui.d.ts} +27 -20
- package/lib/Gui/Gui.js +497 -0
- package/lib/Gui/Gui.js.map +1 -0
- package/lib/Gui/React.d.ts +14 -0
- package/lib/Gui/React.js +89 -0
- package/lib/Gui/React.js.map +1 -0
- package/lib/Gui/Vue.d.ts +13 -0
- package/lib/{RpgGuiCompiled.js → Gui/Vue.js} +64 -11
- package/lib/Gui/Vue.js.map +1 -0
- package/lib/Renderer.js +6 -4
- package/lib/Renderer.js.map +1 -1
- package/lib/RpgClientEngine.js +1 -1
- package/lib/RpgClientEngine.js.map +1 -1
- package/lib/Scene/Scene.d.ts +26 -1
- package/lib/Scene/Scene.js +32 -3
- package/lib/Scene/Scene.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +21 -6
- package/src/Components/Component.ts +6 -0
- package/src/GameEngine.ts +7 -3
- package/src/Gui/Gui.ts +556 -0
- package/src/Gui/React.ts +116 -0
- package/src/Gui/Vue.ts +137 -0
- package/src/Renderer.ts +8 -4
- package/src/RpgClientEngine.ts +1 -1
- package/src/Scene/Scene.ts +35 -4
- package/src/index.ts +2 -1
- package/lib/RpgGui.js +0 -499
- package/lib/RpgGui.js.map +0 -1
- package/lib/RpgGuiCompiled.d.ts +0 -3
- package/lib/RpgGuiCompiled.js.map +0 -1
- package/src/RpgGui.ts +0 -553
- package/src/RpgGuiCompiled.ts +0 -43
package/src/Gui/Gui.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { RpgCommonPlayer, Utils } from '@rpgjs/common'
|
|
2
|
+
import { RpgSound } from '../Sound/RpgSound'
|
|
3
|
+
import { RpgClientEngine, RpgResource } from '../index'
|
|
4
|
+
import { RpgRenderer } from '../Renderer'
|
|
5
|
+
import { GameEngineClient } from '../GameEngine'
|
|
6
|
+
import { SceneMap } from '../Scene/Map'
|
|
7
|
+
import { VueGui } from './Vue'
|
|
8
|
+
import { Scene } from '../Scene/Scene'
|
|
9
|
+
import { map, tap, combineLatest, Subject, filter, Observable } from 'rxjs';
|
|
10
|
+
|
|
11
|
+
const { elementToPositionAbsolute } = Utils
|
|
12
|
+
|
|
13
|
+
interface GuiOptions {
|
|
14
|
+
data: any,
|
|
15
|
+
attachToSprite: boolean
|
|
16
|
+
display: boolean,
|
|
17
|
+
name: string
|
|
18
|
+
isFunction: boolean,
|
|
19
|
+
gui: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface GuiList {
|
|
23
|
+
[guiName: string]: GuiOptions
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const COMPONENT_LIBRARIES: any = [
|
|
27
|
+
VueGui
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export class Gui {
|
|
31
|
+
private renderer: RpgRenderer
|
|
32
|
+
private gameEngine: GameEngineClient
|
|
33
|
+
public clientEngine: RpgClientEngine
|
|
34
|
+
private socket
|
|
35
|
+
public gui: GuiList = {}
|
|
36
|
+
public currentScene: Scene | null = null
|
|
37
|
+
private librariesInstances: any[] = []
|
|
38
|
+
|
|
39
|
+
async _initialize(clientEngine: RpgClientEngine, guiEl: HTMLDivElement) {
|
|
40
|
+
this.clientEngine = clientEngine
|
|
41
|
+
this.renderer = clientEngine.renderer
|
|
42
|
+
this.gameEngine = clientEngine.gameEngine
|
|
43
|
+
const { gui } = this.renderer.options
|
|
44
|
+
|
|
45
|
+
for (let ui of gui) {
|
|
46
|
+
let name = ui.name
|
|
47
|
+
if (Utils.isFunction(ui)) {
|
|
48
|
+
name = Utils.camelToKebab(name)
|
|
49
|
+
}
|
|
50
|
+
this.gui[name] = {
|
|
51
|
+
data: ui.data,
|
|
52
|
+
attachToSprite: ui.rpgAttachToSprite,
|
|
53
|
+
display: false,
|
|
54
|
+
name: name,
|
|
55
|
+
isFunction: Utils.isFunction(ui),
|
|
56
|
+
gui: ui
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (this.clientEngine.envs?.['VITE_REACT']) {
|
|
61
|
+
console.warn('[RPGJS] React GUI is experimental feature. So, its use may change over time. Not yet in production')
|
|
62
|
+
COMPONENT_LIBRARIES.push(await import('./React').then(m => m.ReactGui))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const propagateEvents = (el: HTMLElement) => {
|
|
66
|
+
const events = ['click', 'mousedown', 'mouseup', 'mousemove', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu', 'pointerdown', 'pointerup', 'pointermove', 'pointerenter', 'pointerleave', 'pointerover', 'pointerout', 'pointerupoutside', 'pointercancel', 'touchstart', 'touchend', 'touchmove', 'touchcancel', 'wheel', 'keydown', 'keyup', 'keypress', 'keydownoutside', 'keyupoutside', 'keypressoutside']
|
|
67
|
+
for (let type of events) {
|
|
68
|
+
el.addEventListener(type, (e) => {
|
|
69
|
+
this.renderer.canvas.dispatchEvent(new MouseEvent(type, e))
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let componentClass of COMPONENT_LIBRARIES) {
|
|
75
|
+
const el = document.createElement('div')
|
|
76
|
+
elementToPositionAbsolute(el)
|
|
77
|
+
el.style['pointer-events'] = 'auto'
|
|
78
|
+
propagateEvents(el)
|
|
79
|
+
guiEl.appendChild(el)
|
|
80
|
+
this.librariesInstances.push(new componentClass(el, this))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
guiEl.style['pointer-events'] = 'none'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_setSceneReady(scene: Scene) {
|
|
87
|
+
this.currentScene = scene
|
|
88
|
+
this.librariesInstances.forEach(instance => {
|
|
89
|
+
if (instance._setSceneReady) instance._setSceneReady(scene)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getInjectObject(): any {
|
|
94
|
+
const self = this
|
|
95
|
+
return {
|
|
96
|
+
/**
|
|
97
|
+
* Recovery of the current scene
|
|
98
|
+
*
|
|
99
|
+
* ```js
|
|
100
|
+
* export default {
|
|
101
|
+
* inject: ['rpgScene'],
|
|
102
|
+
* mounted() {
|
|
103
|
+
* const scene = this.rpgScene()
|
|
104
|
+
* scene.stopInputs()
|
|
105
|
+
* }
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @prop {Function returns RpgScene} [rpgScene]
|
|
110
|
+
* @memberof VueInject
|
|
111
|
+
* */
|
|
112
|
+
rpgScene: this.renderer.getScene.bind(this.renderer),
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Retrieve the main container of the game
|
|
116
|
+
*
|
|
117
|
+
* ```js
|
|
118
|
+
* export default {
|
|
119
|
+
* inject: ['rpgStage'],
|
|
120
|
+
* mounted() {
|
|
121
|
+
* const blur = new PIXI.BlurFilter()
|
|
122
|
+
this.rpgStage.filters = [blur]
|
|
123
|
+
* }
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* @prop {PIXI.Container} [rpgStage]
|
|
128
|
+
* @memberof VueInject
|
|
129
|
+
* */
|
|
130
|
+
rpgStage: this.renderer.stage,
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Listen to all the objects present in the room (events and players)
|
|
134
|
+
*
|
|
135
|
+
* ```js
|
|
136
|
+
* export default {
|
|
137
|
+
* inject: ['rpgObjects'],
|
|
138
|
+
* mounted() {
|
|
139
|
+
* this.obs = this.rpgObjects.subscribe((objects) => {
|
|
140
|
+
* for (let id in objects) {
|
|
141
|
+
* const obj = objects[id]
|
|
142
|
+
* console.log(obj.object, obj.paramsChanged)
|
|
143
|
+
* }
|
|
144
|
+
* })
|
|
145
|
+
* },
|
|
146
|
+
* unmounted() {
|
|
147
|
+
* this.obs.unsubscribe()
|
|
148
|
+
* }
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* > remember to unsubscribe for memory leaks
|
|
153
|
+
*
|
|
154
|
+
* It is an observable that returns an object:
|
|
155
|
+
*
|
|
156
|
+
* * the key is the object identifier
|
|
157
|
+
* * The value is an object comprising:
|
|
158
|
+
* * `object`: The entire object
|
|
159
|
+
* * `paramsChanged`: Only the representation of the properties that have been changed on this object
|
|
160
|
+
*
|
|
161
|
+
* @prop {Observable<{ [objectId]: { object: object, paramsChanged: object } }>} [rpgObjects]
|
|
162
|
+
* @memberof VueInject
|
|
163
|
+
* */
|
|
164
|
+
rpgObjects: this.clientEngine.objects,
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Recovers and listens to the current player
|
|
168
|
+
*
|
|
169
|
+
* ```js
|
|
170
|
+
* export default {
|
|
171
|
+
* inject: ['rpgCurrentPlayer'],
|
|
172
|
+
* mounted() {
|
|
173
|
+
* this.obs = this.rpgCurrentPlayer.subscribe((obj) => {
|
|
174
|
+
* console.log(obj.object, obj.paramsChanged)
|
|
175
|
+
* })
|
|
176
|
+
* },
|
|
177
|
+
* unmounted() {
|
|
178
|
+
* this.obs.unsubscribe()
|
|
179
|
+
* }
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* * `object`: The whole player
|
|
184
|
+
* * `paramsChanged`: Only the representation of the properties that have been changed on this player
|
|
185
|
+
*
|
|
186
|
+
* @prop {Observable<{ object: object, paramsChanged: object }>} [rpgCurrentPlayer]
|
|
187
|
+
* @memberof VueInject
|
|
188
|
+
* */
|
|
189
|
+
rpgCurrentPlayer: this.clientEngine.objects
|
|
190
|
+
.pipe(
|
|
191
|
+
map((objects: any) => objects[this.gameEngine.playerId]),
|
|
192
|
+
filter(player => !!player)
|
|
193
|
+
),
|
|
194
|
+
rpgGameEngine: this.gameEngine,
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Tell the server to close the GUI.
|
|
198
|
+
*
|
|
199
|
+
* It is a function with 2 parameters:
|
|
200
|
+
* * `name`: The name of the component
|
|
201
|
+
* * `data`: The data you want to pass to the server
|
|
202
|
+
*
|
|
203
|
+
* ```js
|
|
204
|
+
* export default {
|
|
205
|
+
* inject: ['rpgGuiClose'],
|
|
206
|
+
* methods: {
|
|
207
|
+
* close() {
|
|
208
|
+
* this.rpgGuiClose('gui-name', {
|
|
209
|
+
* amount: 1000
|
|
210
|
+
* })
|
|
211
|
+
* }
|
|
212
|
+
* }
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* @prop {Function(name, data)} [rpgGuiClose]
|
|
217
|
+
* @memberof VueInject
|
|
218
|
+
* */
|
|
219
|
+
rpgGuiClose(name: string, data?) {
|
|
220
|
+
const guiId = name || this.$options?.name
|
|
221
|
+
self.socket.emit('gui.exit', {
|
|
222
|
+
guiId,
|
|
223
|
+
data
|
|
224
|
+
})
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Perform an interaction with the open GUI
|
|
229
|
+
*
|
|
230
|
+
* It is a function with 2 parameters:
|
|
231
|
+
* * `guiId`: The name of the component/Gui
|
|
232
|
+
* * `name`: The name of the interaction (defined on the server side)
|
|
233
|
+
* * `data`: Data to be sent
|
|
234
|
+
*
|
|
235
|
+
* ```js
|
|
236
|
+
* export default {
|
|
237
|
+
* inject: ['rpgGuiInteraction'],
|
|
238
|
+
* methods: {
|
|
239
|
+
* changeGold() {
|
|
240
|
+
* this.rpgGuiInteraction('gui-name', 'change-gold', {
|
|
241
|
+
* amount: 100
|
|
242
|
+
* })
|
|
243
|
+
* }
|
|
244
|
+
* }
|
|
245
|
+
* }
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* @prop {Function(guiId, name, data = {})} [rpgGuiInteraction]
|
|
249
|
+
* @memberof VueInject
|
|
250
|
+
* */
|
|
251
|
+
rpgGuiInteraction: (guiId: string, name: string, data: any = {}) => {
|
|
252
|
+
this.socket.emit('gui.interaction', {
|
|
253
|
+
guiId,
|
|
254
|
+
name,
|
|
255
|
+
data
|
|
256
|
+
})
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Listen to the keys that are pressed on the keyboard
|
|
261
|
+
*
|
|
262
|
+
* ```js
|
|
263
|
+
* export default {
|
|
264
|
+
* inject: ['rpgKeypress'],
|
|
265
|
+
* mounted() {
|
|
266
|
+
* this.obs = this.rpgKeypress.subscribe(({ inputName, control }) => {
|
|
267
|
+
* console.log(inputName) // "escape"
|
|
268
|
+
* console.log(control.actionName) // "back"
|
|
269
|
+
* })
|
|
270
|
+
* },
|
|
271
|
+
* unmounted() {
|
|
272
|
+
* this.obs.unsubscribe()
|
|
273
|
+
* }
|
|
274
|
+
* }
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
277
|
+
* @prop {Observable<{ inputName: string, control: { actionName: string, options: any } }>} [rpgKeypress]
|
|
278
|
+
* @memberof VueInject
|
|
279
|
+
* */
|
|
280
|
+
rpgKeypress: this.clientEngine.keyChange
|
|
281
|
+
.pipe(
|
|
282
|
+
map(name => {
|
|
283
|
+
const control = this.clientEngine.controls.getControl(name)
|
|
284
|
+
return {
|
|
285
|
+
inputName: name,
|
|
286
|
+
control
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
),
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Recovers the socket.
|
|
293
|
+
*
|
|
294
|
+
* ```js
|
|
295
|
+
* export default {
|
|
296
|
+
* inject: ['rpgSocket'],
|
|
297
|
+
* mounted() {
|
|
298
|
+
* const socket = this.rpgSocket()
|
|
299
|
+
* socket.emit('foo', 'bar')
|
|
300
|
+
* }
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*
|
|
304
|
+
* @prop {Function returns RpgScene} [rpgSocket]
|
|
305
|
+
* @memberof VueInject
|
|
306
|
+
* */
|
|
307
|
+
rpgSocket: () => this.socket,
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The RpgGui object to control GUIs
|
|
311
|
+
*
|
|
312
|
+
* ```js
|
|
313
|
+
* export default {
|
|
314
|
+
* inject: ['rpgGui'],
|
|
315
|
+
* mounted() {
|
|
316
|
+
* const guis = this.rpgGui.getAll()
|
|
317
|
+
* }
|
|
318
|
+
* }
|
|
319
|
+
* ```
|
|
320
|
+
*
|
|
321
|
+
* @prop {RpgGui} [rpgGui]
|
|
322
|
+
* @memberof VueInject
|
|
323
|
+
* */
|
|
324
|
+
rpgGui: this,
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Equivalent to RpgSound
|
|
328
|
+
*
|
|
329
|
+
* ```js
|
|
330
|
+
* export default {
|
|
331
|
+
* inject: ['rpgSound'],
|
|
332
|
+
* mounted() {
|
|
333
|
+
* this.rpgSound.get('my-sound-id').play()
|
|
334
|
+
* }
|
|
335
|
+
* }
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @prop {RpgSound} [rpgSound]
|
|
339
|
+
* @memberof VueInject
|
|
340
|
+
* */
|
|
341
|
+
rpgSound: RpgSound,
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Find the game's image and sound library
|
|
345
|
+
*
|
|
346
|
+
* ```js
|
|
347
|
+
* export default {
|
|
348
|
+
* inject: ['rpgResource'],
|
|
349
|
+
* mounted() {
|
|
350
|
+
* const resourceImage = this.rpgResource.spritesheets.get('image_id')
|
|
351
|
+
* const resourceSound = this.rpgResource.sounds.get('sound_id')
|
|
352
|
+
* }
|
|
353
|
+
* }
|
|
354
|
+
* ```
|
|
355
|
+
*
|
|
356
|
+
* @prop { { spritesheets: Map, sounds: Map } } [rpgResource]
|
|
357
|
+
* @memberof VueInject
|
|
358
|
+
* */
|
|
359
|
+
rpgResource: RpgResource,
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get RpgClientEngine instance
|
|
363
|
+
*
|
|
364
|
+
* ```js
|
|
365
|
+
* export default {
|
|
366
|
+
* inject: ['rpgEngine'],
|
|
367
|
+
* mounted() {
|
|
368
|
+
* const vueInstance = this.rpgEngine.vueInstance
|
|
369
|
+
* }
|
|
370
|
+
* }
|
|
371
|
+
* ```
|
|
372
|
+
*
|
|
373
|
+
* @prop {RpgClientEngine} [rpgEngine]
|
|
374
|
+
* @memberof VueInject
|
|
375
|
+
* */
|
|
376
|
+
rpgEngine: this.clientEngine
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** @internal */
|
|
381
|
+
_setSocket(socket) {
|
|
382
|
+
this.socket = socket
|
|
383
|
+
this.socket.on('gui.open', ({ guiId, data }) => {
|
|
384
|
+
this.display(guiId, data)
|
|
385
|
+
})
|
|
386
|
+
this.socket.on('gui.tooltip', ({ players, display }) => {
|
|
387
|
+
for (let playerId of players) {
|
|
388
|
+
const sprite = this.renderer.getScene<SceneMap>()?.getSprite(playerId)
|
|
389
|
+
if (sprite) sprite.guiDisplay = display
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
this.socket.on('gui.exit', (guiId) => {
|
|
393
|
+
this.hide(guiId)
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** @internal */
|
|
398
|
+
_setGui(id, obj) {
|
|
399
|
+
const guiObj = this.get(id)
|
|
400
|
+
if (!guiObj) {
|
|
401
|
+
throw `The GUI named ${id} is non-existent. Please add the component in the gui property of the decorator @RpgClient`
|
|
402
|
+
}
|
|
403
|
+
for (let key in obj) {
|
|
404
|
+
guiObj[key] = obj[key]
|
|
405
|
+
}
|
|
406
|
+
this.librariesInstances.forEach(instance => {
|
|
407
|
+
instance.gui = Object.assign({}, this.gui)
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get a GUI. You retrieve GUI data and information whether it is displayed or not
|
|
413
|
+
*
|
|
414
|
+
* ```ts
|
|
415
|
+
* import { RpgGui } from '@rpgjs/client'
|
|
416
|
+
*
|
|
417
|
+
* const gui = RpgGui.get('my-gui')
|
|
418
|
+
* console.log(gui.display) // false
|
|
419
|
+
* ```
|
|
420
|
+
*
|
|
421
|
+
* @title Get a GUI
|
|
422
|
+
* @method RpgGui.get(id)
|
|
423
|
+
* @param {string} id
|
|
424
|
+
* @returns { { data: any, display: boolean } }
|
|
425
|
+
* @memberof RpgGui
|
|
426
|
+
*/
|
|
427
|
+
get(id) {
|
|
428
|
+
if (typeof id != 'string') {
|
|
429
|
+
id = id.name
|
|
430
|
+
}
|
|
431
|
+
return this.gui[id]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get all GUI. You retrieve GUI data and information whether it is displayed or not
|
|
436
|
+
*
|
|
437
|
+
* ```ts
|
|
438
|
+
* import { RpgGui } from '@rpgjs/client'
|
|
439
|
+
*
|
|
440
|
+
* const gui = RpgGui.getAll()
|
|
441
|
+
* console.log(gui) // { 'rpg-dialog': { data: {}, display: true } }
|
|
442
|
+
* ```
|
|
443
|
+
*
|
|
444
|
+
* @title Get all GUI
|
|
445
|
+
* @method RpgGui.getAll()
|
|
446
|
+
* @returns { { [guiName]: { data: any, display: boolean } }}
|
|
447
|
+
* @memberof RpgGui
|
|
448
|
+
*/
|
|
449
|
+
getAll() {
|
|
450
|
+
return this.gui
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Checks if the GUI exists RpgClient's gui array
|
|
455
|
+
*
|
|
456
|
+
* ```ts
|
|
457
|
+
* import { RpgGui } from '@rpgjs/client'
|
|
458
|
+
*
|
|
459
|
+
* RpgGui.exists('my-gui') // true
|
|
460
|
+
* ```
|
|
461
|
+
*
|
|
462
|
+
* @title GUI Exists ?
|
|
463
|
+
* @method RpgGui.exists(id)
|
|
464
|
+
* @param {string} id
|
|
465
|
+
* @returns {boolean}
|
|
466
|
+
* @memberof RpgGui
|
|
467
|
+
*/
|
|
468
|
+
exists(id: string): boolean {
|
|
469
|
+
return !!this.get(id)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Calls a GUI according to identifier. You can send retrievable data in the component
|
|
474
|
+
*
|
|
475
|
+
* ```ts
|
|
476
|
+
* import { RpgGui } from '@rpgjs/client'
|
|
477
|
+
*
|
|
478
|
+
* RpgGui.display('my-gui')
|
|
479
|
+
* ```
|
|
480
|
+
*
|
|
481
|
+
* @title Display GUI
|
|
482
|
+
* @method RpgGui.display(id,data)
|
|
483
|
+
* @param {string} id
|
|
484
|
+
* @param {object} [data]
|
|
485
|
+
* @returns {void}
|
|
486
|
+
* @memberof RpgGui
|
|
487
|
+
*/
|
|
488
|
+
display(id: string, data = {}) {
|
|
489
|
+
this._setGui(id, {
|
|
490
|
+
display: true,
|
|
491
|
+
data
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Hide a GUI according to its identifier
|
|
497
|
+
*
|
|
498
|
+
* ```ts
|
|
499
|
+
* import { RpgGui } from '@rpgjs/client'
|
|
500
|
+
*
|
|
501
|
+
* RpgGui.hide('my-gui')
|
|
502
|
+
* ```
|
|
503
|
+
*
|
|
504
|
+
* @title Hide GUI
|
|
505
|
+
* @method RpgGui.hide(id)
|
|
506
|
+
* @param {string} id
|
|
507
|
+
* @returns {void}
|
|
508
|
+
* @memberof RpgGui
|
|
509
|
+
*/
|
|
510
|
+
hide(id: string) {
|
|
511
|
+
this._setGui(id, {
|
|
512
|
+
display: false
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** @internal */
|
|
517
|
+
clear() {
|
|
518
|
+
this.gui = {}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** @internal */
|
|
522
|
+
tooltipPosition(position: { x: number, y: number }) {
|
|
523
|
+
const scene = this.renderer.getScene<SceneMap>()
|
|
524
|
+
const viewport = scene?.viewport
|
|
525
|
+
if (viewport) {
|
|
526
|
+
const currentZoom = viewport.scale.x
|
|
527
|
+
const left = (position.x - viewport.left) * currentZoom
|
|
528
|
+
const top = (position.y - viewport.top) * currentZoom
|
|
529
|
+
return {
|
|
530
|
+
transform: `translate(${left}px,${top}px)`
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return {}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** @internal */
|
|
537
|
+
tooltipFilter(sprites: RpgCommonPlayer[]): RpgCommonPlayer[] {
|
|
538
|
+
return sprites.filter(tooltip => tooltip.guiDisplay)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** @internal */
|
|
542
|
+
get listenTooltipObjects(): Observable<RpgCommonPlayer[]> {
|
|
543
|
+
return combineLatest(
|
|
544
|
+
[
|
|
545
|
+
this.clientEngine.gameEngine.all,
|
|
546
|
+
this.currentScene?.objectsMoving as Subject<any>
|
|
547
|
+
]
|
|
548
|
+
).pipe(
|
|
549
|
+
map(([objects]) => {
|
|
550
|
+
return Object.values(objects).map((obj: any) => obj.object)
|
|
551
|
+
})
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const RpgGui = new Gui()
|
package/src/Gui/React.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createRoot } from 'react-dom/client';
|
|
2
|
+
import { createElement, Fragment, useState, createContext, useEffect, useContext, useCallback, useSyncExternalStore, useRef } from 'react'
|
|
3
|
+
import { RpgClientEngine } from '../RpgClientEngine';
|
|
4
|
+
import { RpgRenderer } from '../Renderer';
|
|
5
|
+
import { BehaviorSubject, map, tap, combineLatest, Subject } from 'rxjs';
|
|
6
|
+
import type { Gui } from './Gui';
|
|
7
|
+
|
|
8
|
+
export { useStore } from '@nanostores/react'
|
|
9
|
+
export const RpgReactContext = createContext({} as any)
|
|
10
|
+
|
|
11
|
+
// TODO
|
|
12
|
+
export const useObjects = () => {
|
|
13
|
+
const [objects, setObjects] = useState([] as any[])
|
|
14
|
+
const { rpgObjects } = useContext(RpgReactContext)
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
rpgObjects
|
|
17
|
+
.pipe(
|
|
18
|
+
map((objects: any) => Object.values(objects).map((obj: any) => obj.object))
|
|
19
|
+
)
|
|
20
|
+
.subscribe(setObjects)
|
|
21
|
+
}, [])
|
|
22
|
+
return objects
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// TODO
|
|
26
|
+
export const useCurrentPlayer = () => {
|
|
27
|
+
const { rpgCurrentPlayer } = useContext(RpgReactContext);
|
|
28
|
+
|
|
29
|
+
const currentPlayerRef = useRef({});
|
|
30
|
+
let _onChanges
|
|
31
|
+
|
|
32
|
+
const subscribe = (onChanges) => {
|
|
33
|
+
_onChanges = onChanges
|
|
34
|
+
return () => {
|
|
35
|
+
_onChanges = null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const ob$ = rpgCurrentPlayer
|
|
41
|
+
.pipe(
|
|
42
|
+
map((player: any) => player.object),
|
|
43
|
+
tap((player: any) => currentPlayerRef.current = player)
|
|
44
|
+
);
|
|
45
|
+
const subscription = ob$.subscribe(() => {
|
|
46
|
+
_onChanges?.()
|
|
47
|
+
});
|
|
48
|
+
return () => subscription.unsubscribe();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
return useSyncExternalStore(subscribe, () => currentPlayerRef.current);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ReactGui {
|
|
55
|
+
private app: any
|
|
56
|
+
private clientEngine: RpgClientEngine
|
|
57
|
+
private renderer: RpgRenderer
|
|
58
|
+
private _gui: BehaviorSubject<any[]> = new BehaviorSubject([] as any)
|
|
59
|
+
//private _tooltips: BehaviorSubject<any[]> = new BehaviorSubject([] as any)
|
|
60
|
+
|
|
61
|
+
constructor(rootEl: HTMLDivElement, parentGui: Gui) {
|
|
62
|
+
this.app = createRoot(rootEl)
|
|
63
|
+
this.clientEngine = parentGui.clientEngine
|
|
64
|
+
this.renderer = this.clientEngine.renderer
|
|
65
|
+
|
|
66
|
+
const GuiTooltip = (ui): any => {
|
|
67
|
+
return () => {
|
|
68
|
+
const [_tooltip, setTooltip] = useState<any[]>([])
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
parentGui.listenTooltipObjects.subscribe(setTooltip)
|
|
71
|
+
// force combineLatest to emit first value
|
|
72
|
+
parentGui.currentScene?.objectsMoving.next({})
|
|
73
|
+
}, [parentGui.currentScene])
|
|
74
|
+
return parentGui.tooltipFilter(_tooltip).map(sprite => createElement('div', {
|
|
75
|
+
style: parentGui.tooltipPosition({ x: sprite.position.x, y: sprite.position.y }),
|
|
76
|
+
key: sprite.id,
|
|
77
|
+
}, createElement(ui.gui, {
|
|
78
|
+
spriteData: sprite,
|
|
79
|
+
...(ui.data || {}),
|
|
80
|
+
})))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const GuiWrapper = () => {
|
|
85
|
+
const [_gui, setGui] = useState<any[]>([])
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
this._gui.subscribe(gui => setGui(gui))
|
|
88
|
+
}, [])
|
|
89
|
+
return createElement(RpgReactContext.Provider, {
|
|
90
|
+
value: parentGui.getInjectObject()
|
|
91
|
+
},
|
|
92
|
+
..._gui.filter(ui => ui.display && !ui.attachToSprite).map(ui => createElement(ui.gui, {
|
|
93
|
+
key: ui.name,
|
|
94
|
+
...(ui.data || {})
|
|
95
|
+
})),
|
|
96
|
+
..._gui.filter(ui => ui.display && ui.attachToSprite).map(ui => createElement('div', {
|
|
97
|
+
key: ui.name
|
|
98
|
+
}, createElement(GuiTooltip(ui)))),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.app.render(
|
|
103
|
+
createElement(GuiWrapper)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
set gui(val) {
|
|
108
|
+
let array: any = []
|
|
109
|
+
for (let key in val) {
|
|
110
|
+
// ignore vuejs component
|
|
111
|
+
if (!val[key].isFunction) continue
|
|
112
|
+
array.push(val[key])
|
|
113
|
+
}
|
|
114
|
+
this._gui.next(array)
|
|
115
|
+
}
|
|
116
|
+
}
|