@rpgjs/common 3.3.2 → 4.0.0-beta.10

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.
Files changed (80) hide show
  1. package/LICENSE +19 -0
  2. package/lib/AbstractObject.d.ts +3 -2
  3. package/lib/AbstractObject.js +296 -323
  4. package/lib/AbstractObject.js.map +1 -1
  5. package/lib/Color.js +1 -5
  6. package/lib/Color.js.map +1 -1
  7. package/lib/Event.js +2 -6
  8. package/lib/Event.js.map +1 -1
  9. package/lib/EventEmitter.js +3 -7
  10. package/lib/EventEmitter.js.map +1 -1
  11. package/lib/Game.js +73 -85
  12. package/lib/Game.js.map +1 -1
  13. package/lib/Hit.js +21 -28
  14. package/lib/Hit.js.map +1 -1
  15. package/lib/Logger.js +2 -7
  16. package/lib/Logger.js.map +1 -1
  17. package/lib/Map.d.ts +1 -0
  18. package/lib/Map.js +29 -53
  19. package/lib/Map.js.map +1 -1
  20. package/lib/Module.d.ts +2 -2
  21. package/lib/Module.js +84 -97
  22. package/lib/Module.js.map +1 -1
  23. package/lib/Player.d.ts +1 -0
  24. package/lib/Player.js +4 -7
  25. package/lib/Player.js.map +1 -1
  26. package/lib/Plugin.d.ts +4 -3
  27. package/lib/Plugin.js +14 -14
  28. package/lib/Plugin.js.map +1 -1
  29. package/lib/Scheduler.js +14 -21
  30. package/lib/Scheduler.js.map +1 -1
  31. package/lib/Shape.d.ts +1 -1
  32. package/lib/Shape.js +39 -52
  33. package/lib/Shape.js.map +1 -1
  34. package/lib/Utils.d.ts +4 -1
  35. package/lib/Utils.js +38 -53
  36. package/lib/Utils.js.map +1 -1
  37. package/lib/Vector2d.js +3 -8
  38. package/lib/Vector2d.js.map +1 -1
  39. package/lib/VirtualGrid.d.ts +1 -1
  40. package/lib/VirtualGrid.js +5 -12
  41. package/lib/VirtualGrid.js.map +1 -1
  42. package/lib/Worker.js +2 -10
  43. package/lib/Worker.js.map +1 -1
  44. package/lib/WorldMaps.d.ts +3 -1
  45. package/lib/WorldMaps.js +29 -15
  46. package/lib/WorldMaps.js.map +1 -1
  47. package/lib/gui/PrebuiltGui.js +2 -5
  48. package/lib/gui/PrebuiltGui.js.map +1 -1
  49. package/lib/index.d.ts +3 -1
  50. package/lib/index.js +24 -69
  51. package/lib/index.js.map +1 -1
  52. package/lib/transports/io.js +5 -9
  53. package/lib/transports/io.js.map +1 -1
  54. package/lib/workers/move.js +26 -42
  55. package/lib/workers/move.js.map +1 -1
  56. package/package.json +9 -11
  57. package/src/AbstractObject.ts +915 -0
  58. package/src/Color.ts +29 -0
  59. package/src/DefaultInput.ts +26 -0
  60. package/src/Event.ts +3 -0
  61. package/src/EventEmitter.ts +52 -0
  62. package/src/Game.ts +150 -0
  63. package/src/Hit.ts +70 -0
  64. package/src/Logger.ts +7 -0
  65. package/src/Map.ts +335 -0
  66. package/src/Module.ts +108 -0
  67. package/src/Player.ts +30 -0
  68. package/src/Plugin.ts +91 -0
  69. package/src/Scheduler.ts +88 -0
  70. package/src/Shape.ts +300 -0
  71. package/src/Utils.ts +168 -0
  72. package/src/Vector2d.ts +70 -0
  73. package/src/VirtualGrid.ts +78 -0
  74. package/src/Worker.ts +17 -0
  75. package/src/WorldMaps.ts +204 -0
  76. package/src/gui/PrebuiltGui.ts +27 -0
  77. package/src/index.ts +24 -0
  78. package/src/transports/io.ts +91 -0
  79. package/src/workers/move.ts +61 -0
  80. package/tsconfig.json +25 -0
@@ -0,0 +1,915 @@
1
+ import { intersection, generateUID, toRadians, isInstanceOf } from './Utils'
2
+ import { Hit, HitType } from './Hit'
3
+ import { RpgShape } from './Shape'
4
+ import SAT from 'sat'
5
+ import { TileInfo, RpgCommonMap } from './Map'
6
+ import { RpgPlugin, HookServer } from './Plugin'
7
+ import { GameSide, RpgCommonGame } from './Game'
8
+ import { Vector2d, Vector2dZero } from './Vector2d'
9
+ import { Box } from './VirtualGrid'
10
+ import { Behavior, ClientMode, Direction, MoveClientMode, MoveTo, PlayerType, Position, PositionXY, Tick } from '@rpgjs/types'
11
+ import { from, map, mergeMap, Observable, Subject, tap, takeUntil } from 'rxjs'
12
+
13
+ const ACTIONS = { IDLE: 0, RUN: 1, ACTION: 2 }
14
+
15
+ type CollisionOptions = {
16
+ collision?: (event: AbstractObject) => void
17
+ near?: (event: AbstractObject) => void,
18
+ allSearch?: boolean
19
+ }
20
+
21
+ export class AbstractObject {
22
+ map: string = ''
23
+ height: number = 0
24
+ width: number = 0
25
+ speed: number
26
+ direction: number = 3
27
+
28
+ /*
29
+ Properties for move mode
30
+ */
31
+ checkCollision: boolean = true
32
+ clientModeMove: ClientMode = MoveClientMode.ByDirection
33
+ behavior: Behavior = Behavior.Direction
34
+
35
+ hitbox: SAT.Box
36
+
37
+ inShapes: {
38
+ [shapeId: string]: RpgShape
39
+ } = {}
40
+
41
+ disableVirtualGrid: boolean = false
42
+
43
+ private shapes: RpgShape[] = []
44
+ private _position: Vector2d
45
+ private _hitboxPos: SAT.Vector
46
+ private collisionWith: AbstractObject[] = []
47
+ private _collisionWithTiles: TileInfo[] = []
48
+ private _collisionWithShapes: RpgShape[] = []
49
+
50
+ private destroyMove$: Subject<boolean> = new Subject<boolean>()
51
+ // notifier for destroy
52
+ _destroy$: Subject<void> = new Subject()
53
+
54
+
55
+ static get ACTIONS() {
56
+ return ACTIONS
57
+ }
58
+
59
+ constructor(private gameEngine: RpgCommonGame, public playerId: string) {
60
+ this._hitboxPos = new SAT.Vector(0, 0)
61
+ this.setHitbox(this.width, this.height)
62
+ this.position = { x: 0, y: 0, z: 0 }
63
+ }
64
+
65
+ get id() {
66
+ return this.playerId
67
+ }
68
+
69
+ set id(str: string) {
70
+ this.playerId = str
71
+ }
72
+
73
+ updateInVirtualGrid() {
74
+ const map = this.mapInstance
75
+ if (map && !this.disableVirtualGrid /*&& this.gameEngine.isWorker TODO */) {
76
+ map.grid.insertInCells(this.id, this.getSizeMaxShape())
77
+ }
78
+ }
79
+
80
+ get canMove(): boolean {
81
+ return this.clientModeMove == MoveClientMode.ByDirection
82
+ }
83
+
84
+ set canMove(val: boolean) {
85
+ this.clientModeMove = val ? MoveClientMode.ByDirection : MoveClientMode.Disabled
86
+ }
87
+
88
+ /**
89
+ * Get/Set position x, y and z of player
90
+ *
91
+ * z is the depth layer. By default, its value is 0. Collisions and overlays will be performed with other objects on the same z-position.
92
+ *
93
+ * @title Get/Set position
94
+ * @prop { { x: number, y: number, z: number } } position
95
+ * @memberof Player
96
+ */
97
+ set position(val: Position | Vector2d) {
98
+ const { x, y, z } = val
99
+ if (!isInstanceOf(val, Vector2d)) {
100
+ val = new Vector2d(x, y, z)
101
+ }
102
+ this._hitboxPos.x = x
103
+ this._hitboxPos.y = y
104
+ this._hitboxPos.z = z
105
+ this.updateInVirtualGrid()
106
+ this._position = new Proxy<Vector2d>(val as Vector2d, {
107
+ get: (target, prop: string) => target[prop],
108
+ set: (target, prop, value) => {
109
+ this._hitboxPos[prop] = value
110
+ target[prop] = value
111
+ return true
112
+ }
113
+ })
114
+ }
115
+
116
+ get position(): Vector2d {
117
+ return this._position
118
+ }
119
+
120
+ get worldPositionX(): number {
121
+ let x = this.position.x
122
+ if (this.mapInstance) {
123
+ x += this.mapInstance.worldX
124
+ }
125
+ return x
126
+ }
127
+
128
+ get worldPositionY(): number {
129
+ let y = this.position.y
130
+ if (this.mapInstance) {
131
+ y += this.mapInstance.worldY
132
+ }
133
+ return y
134
+ }
135
+
136
+ set posX(val) {
137
+ this.position.x = val
138
+ }
139
+
140
+ set posY(val) {
141
+ this.position.y = val
142
+ }
143
+
144
+ set posZ(val) {
145
+ this.position.z = val
146
+ }
147
+
148
+ /** @internal */
149
+ get mapInstance(): RpgCommonMap {
150
+ if (this.gameEngine.side == GameSide.Client) {
151
+ return RpgCommonMap.bufferClient.get(this.map)
152
+ }
153
+ return RpgCommonMap.buffer.get(this.map)
154
+ }
155
+
156
+ /**
157
+ *
158
+ * Recovers all the colliding shapes of the current player
159
+ *
160
+ * @title Get Collision of shapes
161
+ * @since 3.2.0
162
+ * @readonly
163
+ * @prop { RpgShape[] } shapes
164
+ * @memberof Player
165
+ * @memberof RpgSpriteLogic
166
+ */
167
+ get shapesCollision(): RpgShape[] {
168
+ return this._collisionWithShapes
169
+ }
170
+
171
+ /**
172
+ *
173
+ * Recovers all the colliding tiles of the current player
174
+ *
175
+ * @title Get Collision of tiles
176
+ * @since 3.0.0-beta.4
177
+ * @readonly
178
+ * @prop { TileInfo[] } tiles
179
+ * @memberof Player
180
+ * @memberof RpgSpriteLogic
181
+ */
182
+ get tilesCollision(): TileInfo[] {
183
+ return this._collisionWithTiles
184
+ }
185
+
186
+ /**
187
+ *
188
+ * Recovers all other players and events colliding with the current player's hitbox
189
+ *
190
+ * @title Get Collision of other players/events
191
+ * @since 3.0.0-beta.4
192
+ * @readonly
193
+ * @prop { (RpgPlayer | RpgEvent)[] } otherPlayersCollision
194
+ * @memberof Player
195
+ * @memberof RpgSpriteLogic
196
+ */
197
+ get otherPlayersCollision(): AbstractObject[] {
198
+ return this.collisionWith
199
+ }
200
+
201
+ /**
202
+ * Define the size of the player. You can set the hitbox for collisions
203
+ *
204
+ * ```ts
205
+ * player.setSizes({
206
+ * width: 32,
207
+ * height: 32
208
+ * })
209
+ * ```
210
+ *
211
+ * and with hitbox:
212
+ *
213
+ * ```ts
214
+ * player.setSizes({
215
+ * width: 32,
216
+ * height: 32,
217
+ * hitbox: {
218
+ * width: 20,
219
+ * height: 20
220
+ * }
221
+ * })
222
+ * ```
223
+ *
224
+ * @title Set Sizes
225
+ * @method player.setSizes(key,value)
226
+ * @param { { width: number, height: number, hitbox?: { width: number, height: number } } } obj
227
+ * @deprecated
228
+ * @returns {void}
229
+ * @memberof Player
230
+ */
231
+ setSizes(obj: { width: number, height: number, hitbox?: { width: number, height: number } }): void {
232
+ this.width = obj.width
233
+ this.height = obj.height
234
+ if (obj.hitbox) {
235
+ this.hitbox = new SAT.Box(this._hitboxPos, obj.hitbox.width, obj.hitbox.height)
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Define the hitbox of the player.
241
+ *
242
+ * ```ts
243
+ * player.setHitbox(20, 20)
244
+ * ```
245
+ *
246
+ * @title Set Hitbox
247
+ * @method player.setHitbox(width,height)
248
+ * @param {number} width
249
+ * @param {number} height
250
+ * @returns {void}
251
+ * @memberof Player
252
+ */
253
+ setHitbox(width: number, height: number): void {
254
+ const map = this.mapInstance
255
+ if (map) {
256
+ this.width = map.tileWidth
257
+ this.height = map.tileHeight
258
+ }
259
+ this.hitbox = new SAT.Box(this._hitboxPos, width, height)
260
+ this.wHitbox = width
261
+ this.hHitbox = height
262
+ this.updateInVirtualGrid()
263
+ }
264
+
265
+ set wHitbox(val) {
266
+ this.hitbox.w = val
267
+ }
268
+
269
+ set hHitbox(val) {
270
+ this.hitbox.h = val
271
+ }
272
+
273
+ get wHitbox() {
274
+ return this.hitbox.w
275
+ }
276
+
277
+ get hHitbox() {
278
+ return this.hitbox.h
279
+ }
280
+
281
+ private directionToAngle(direction: number): number {
282
+ const angle = (direction < 2 ? +direction + 2 : direction - 2) * 90
283
+ return toRadians(angle)
284
+ }
285
+
286
+ /** @internal */
287
+ defineNextPosition(direction: number, deltaTimeInt: number): Vector2d {
288
+ const angle = this.directionToAngle(direction)
289
+ const computePosition = (prop: string) => {
290
+ return this.position[prop] + this.speed * deltaTimeInt
291
+ * (Math.round(Math[prop == 'x' ? 'cos' : 'sin'](angle) * 100) / 100)
292
+ }
293
+ return new Vector2d(~~computePosition('x'), ~~computePosition('y'), ~~this.position.z)
294
+ }
295
+
296
+ /** @internal */
297
+ setPosition({ x, y, tileX, tileY }, move = true) {
298
+ const { tileWidth, tileHeight } = this.mapInstance
299
+ if (x !== undefined) this.posX = x
300
+ if (y !== undefined) this.posY = y
301
+ if (tileX !== undefined) this.posX = tileX * tileWidth
302
+ if (tileY !== undefined) this.posY = tileY * tileHeight
303
+ }
304
+
305
+ /** @internal */
306
+ async triggerCollisionWith(type?: number) {
307
+ for (let collisionWith of this.collisionWith) {
308
+ if (collisionWith instanceof RpgShape) {
309
+ const goMap = collisionWith.getProperty<string>('go-map')
310
+ if (goMap && 'changeMap' in this) await this.changeMap(goMap)
311
+ }
312
+ else {
313
+ if (type == AbstractObject.ACTIONS.ACTION) {
314
+ if ('onAction' in collisionWith) await collisionWith.execMethod('onAction', [this])
315
+ }
316
+ else if ('onPlayerTouch' in collisionWith) await collisionWith.execMethod('onPlayerTouch', [this])
317
+ }
318
+ }
319
+ }
320
+
321
+ /** @internal */
322
+ zCollision(other: Pick<AbstractObject, 'height'> & { position: Pick<AbstractObject['position'], 'z'> }): boolean {
323
+ const z = this.position.z
324
+ const otherZ = other.position.z
325
+ return intersection([z, z + this.height], [otherZ, otherZ + other.height])
326
+ }
327
+
328
+ /** @internal */
329
+ moveByDirection(direction: Direction, deltaTimeInt: number): Promise<boolean> {
330
+ const nextPosition = this.defineNextPosition(direction, deltaTimeInt)
331
+ return this.move(nextPosition)
332
+ }
333
+
334
+ /**
335
+ * Retrieves a tile and checks if the player has a collision
336
+ *
337
+ * ```ts
338
+ * const tileInfo = player.getTile(20, 30)
339
+ * console.log(tileInfo)
340
+ * ```
341
+ *
342
+ * Example of returns:
343
+ *
344
+ ```ts
345
+ {
346
+ tiles: [
347
+ {
348
+ id: 0,
349
+ terrain: [],
350
+ probability: null,
351
+ properties: [Object],
352
+ animations: [],
353
+ objectGroups: [],
354
+ image: null,
355
+ gid: 1
356
+ }
357
+ ],
358
+ hasCollision: false,
359
+ isOverlay: undefined,
360
+ objectGroups: [],
361
+ isClimbable: undefined,
362
+ tileIndex: 93
363
+ }
364
+ ```
365
+ *
366
+ * @title Get Tile
367
+ * @since 3.0.0-beta.4
368
+ * @method player.getTile(x,y,z?)
369
+ * @param {number} x
370
+ * @param {number} y
371
+ * @param {number} [z]
372
+ * @returns {object}
373
+ * @memberof Player
374
+ * @memberof RpgSpriteLogic
375
+ */
376
+ getTile(x: number, y: number, z: number = 0, hitbox?: SAT.Box): TileInfo {
377
+ const map = this.mapInstance
378
+ return map.getTile(hitbox || this.hitbox, x, y, [z, this.height])
379
+ }
380
+
381
+ private async collisionObjects(
382
+ playerSizeBox: Box,
383
+ hitbox: SAT.Box,
384
+ triggers?: CollisionOptions
385
+ ): Promise<boolean> {
386
+ const map = this.mapInstance
387
+
388
+ if (!map) return true
389
+
390
+ const events: { [id: string]: AbstractObject } = this.gameEngine.world.getObjectsOfGroup(this.map, this)
391
+ const objects = map.grid.getObjectsByBox(playerSizeBox)
392
+ let boolFound = false
393
+
394
+ for (let objectId of objects) {
395
+ // client side: read "object" propertie
396
+ if (!events[objectId]) continue
397
+ const event = events[objectId]['object'] || events[objectId]
398
+
399
+ if (event.id == this.id) continue
400
+ if (!this.zCollision(event)) continue
401
+
402
+ const collided = Hit.testPolyCollision(HitType.Box, hitbox, event.hitbox)
403
+
404
+ for (let shape of this.shapes) {
405
+ await this.collisionWithShape(shape, event)
406
+ }
407
+
408
+ for (let shape of event.shapes) {
409
+ await event.collisionWithShape(shape, this)
410
+ }
411
+
412
+ if (triggers?.near) triggers.near(event)
413
+
414
+ if (collided) {
415
+ this.collisionWith.push(event)
416
+ this.triggerCollisionWith()
417
+ let throughOtherPlayer = false
418
+ if (event.type == PlayerType.Player && this.type == PlayerType.Player) {
419
+ if (!(event.throughOtherPlayer || this.throughOtherPlayer)) {
420
+ boolFound = true
421
+ if (!triggers?.allSearch) return true
422
+ }
423
+ else {
424
+ throughOtherPlayer = true
425
+ }
426
+ }
427
+ if (!throughOtherPlayer && (!(event.through || this.through))) {
428
+ boolFound = true
429
+ if (!triggers?.allSearch) return true
430
+ }
431
+ }
432
+
433
+ if (boolFound) {
434
+ if (triggers?.collision) triggers.collision(event)
435
+ }
436
+ }
437
+
438
+ return boolFound
439
+ }
440
+
441
+ /** @internal */
442
+ private async collisionWithShape(shape: RpgShape, player: AbstractObject, nextPosition?: Vector2d): Promise<boolean> {
443
+ const collision = shape.hasCollision
444
+ const z = shape.z
445
+ if (shape.isShapePosition()) return false
446
+ if (z !== undefined && !this.zCollision({
447
+ position: { z },
448
+ height: this.mapInstance.zTileHeight
449
+ })) {
450
+ return false
451
+ }
452
+ let position: Vector2d
453
+ let { hitbox } = player
454
+ if (nextPosition) {
455
+ position = nextPosition.copy()
456
+ }
457
+ else {
458
+ position = player.position.copy()
459
+ }
460
+ const hitboxObj = Hit.createObjectHitbox(
461
+ position.x,
462
+ position.y,
463
+ position.z,
464
+ hitbox.w,
465
+ hitbox.h
466
+ )
467
+ let collided = Hit.testPolyCollision(shape.type, hitboxObj, shape.hitbox)
468
+ const playerPositionSaved = player.position.copy()
469
+
470
+ // Position can changed after enter or exit shape. So, we need to verify if position changed and update if z is changed
471
+ // If X or Y changed, we need to return true, it means that stop the current movement, and apply the new position
472
+ const verifyIfPositionChanged = (): boolean | undefined => {
473
+ if (this.position.z != playerPositionSaved.z && nextPosition) {
474
+ nextPosition.z = this.position.z
475
+ }
476
+ if (this.position.x != playerPositionSaved.x || this.position.y != playerPositionSaved.y) {
477
+ return true
478
+ }
479
+ }
480
+
481
+ if (collided) {
482
+ this._collisionWithShapes.push(shape)
483
+ // TODO: in shape after map load
484
+ if (!collision) await shape.in(this)
485
+ if (verifyIfPositionChanged() === true) return true
486
+ this.triggerCollisionWith()
487
+ if (collision) return true
488
+ }
489
+ else {
490
+ await shape.out(this)
491
+ if (verifyIfPositionChanged() === true) return true
492
+ }
493
+
494
+ return false
495
+ }
496
+
497
+ private async collisionShapes(playerSizeBox: Box, nextPosition?: Vector2d, triggers?: CollisionOptions): Promise<boolean> {
498
+ const map = this.mapInstance
499
+ if (!map) return false
500
+ const shapes: { [id: string]: RpgShape } = this.gameEngine.world.getShapesOfGroup(this.map)
501
+ const shapesInGrid = this.gameEngine.side == GameSide.Client
502
+ ? new Set(Object.keys(shapes))
503
+ : map.gridShapes.getObjectsByBox(playerSizeBox)
504
+ let boolFound = false
505
+
506
+ for (let shapeId of shapesInGrid) {
507
+ const shape = shapes[shapeId]['object'] || shapes[shapeId]
508
+ if (triggers?.near) triggers.near(shape)
509
+ const bool = await this.collisionWithShape(shape, this, nextPosition)
510
+ if (bool) {
511
+ if (triggers?.collision) triggers.collision(shape)
512
+ boolFound = true
513
+ if (!triggers?.allSearch) return true
514
+ }
515
+ }
516
+ return boolFound
517
+ }
518
+
519
+ async computeNextPositionByTarget(nextPosition: Vector2d, target: Vector2d): Promise<Vector2d> {
520
+ const pullDistance = target.distanceWith(nextPosition)
521
+ if (pullDistance <= this.speed) {
522
+ return nextPosition.set(target)
523
+ }
524
+ const pull = (target.copy().subtract(nextPosition)).multiply((1 / pullDistance))
525
+ const totalPush = new Vector2dZero()
526
+ let contenders = 0
527
+ const hitbox = Hit.createObjectHitbox(nextPosition.x, nextPosition.y, nextPosition.z, this.hitbox.w, this.hitbox.h)
528
+
529
+ const createObstacle = function (x: number, y: number, radius: number): Vector2d {
530
+ const obstacle = new Vector2d(x, y)
531
+ let push = nextPosition.copy().subtract(obstacle)
532
+ let distance = (nextPosition.distanceWith(obstacle) - radius) - radius;
533
+ if (distance < radius * 2 * 10) {
534
+ ++contenders
535
+ if (distance < 0.0001) distance = 0.0001 // avoid div by 0
536
+ let weight = 1 / distance;
537
+ totalPush.add(push.multiply(weight))
538
+ }
539
+ return obstacle
540
+ }
541
+
542
+ const area = this.mapInstance.tileheight * 2
543
+ this.mapInstance.gridTiles.getCells({
544
+ minX: nextPosition.x - area,
545
+ maxX: nextPosition.x + area,
546
+ minY: nextPosition.y - area,
547
+ maxY: nextPosition.y + area
548
+ }, (index) => {
549
+ if (index < 0) return
550
+ const pos = this.mapInstance.getTilePosition(index)
551
+ const hitbox = Hit.createObjectHitbox(pos.x, pos.y, nextPosition.z, this.hitbox.w, this.hitbox.h)
552
+ const radius = this.mapInstance.tilewidth / 2
553
+ const tile = this.getTile(pos.x, pos.y, nextPosition.z, hitbox)
554
+ if (tile.hasCollision) {
555
+ createObstacle(pos.x, pos.y, radius)
556
+ }
557
+ })
558
+
559
+
560
+ const playerSizeBox = this.getSizeMaxShape(nextPosition.x, nextPosition.y)
561
+
562
+ await this.collisionObjects(playerSizeBox, hitbox, {
563
+ collision: (event: AbstractObject) => {
564
+ const { x, y } = event.position
565
+ createObstacle(x, y, event.hitbox.w)
566
+ },
567
+ allSearch: true
568
+ })
569
+
570
+ await this.collisionShapes(playerSizeBox, nextPosition, {
571
+ collision: (shape) => {
572
+ const { x, y } = shape.position
573
+ createObstacle(x, y, shape.hitbox.w)
574
+ },
575
+ allSearch: true
576
+ })
577
+
578
+ pull
579
+ .multiply(Math.max(1, 4 * contenders))
580
+ .add(totalPush)
581
+ .normalize()
582
+
583
+ return nextPosition.add(pull.multiply(this.speed))
584
+ }
585
+
586
+ async isCollided(nextPosition: Vector2d, options: CollisionOptions = {}): Promise<boolean> {
587
+ this.collisionWith = []
588
+ this._collisionWithTiles = []
589
+ const prevMapId = this.map
590
+ const hitbox = Hit.createObjectHitbox(nextPosition.x, nextPosition.y, 0, this.hitbox.w, this.hitbox.h)
591
+ const boundingMap = this.mapInstance.boundingMap(nextPosition, this.hitbox)
592
+ let collided = false
593
+
594
+ if (boundingMap?.bounding) {
595
+ this.position.set(nextPosition)
596
+ if (!options.allSearch) return true
597
+ else collided = true
598
+ }
599
+
600
+ const tileCollision = (x: number, y: number): boolean => {
601
+ const tile = this.getTile(x, y, nextPosition.z, hitbox)
602
+ if (tile.hasCollision) {
603
+ this._collisionWithTiles.push(tile)
604
+ return true
605
+ }
606
+ return false
607
+ }
608
+
609
+ if (
610
+ tileCollision(nextPosition.x, nextPosition.y) ||
611
+ tileCollision(nextPosition.x + this.hitbox.w, nextPosition.y) ||
612
+ tileCollision(nextPosition.x, nextPosition.y + this.hitbox.h) ||
613
+ tileCollision(nextPosition.x + this.hitbox.w, nextPosition.y + this.hitbox.h)
614
+ ) {
615
+ if (!options.allSearch) return true
616
+ else collided = true
617
+ }
618
+
619
+ if (this.autoChangeMap) {
620
+ const changeMap = await this.autoChangeMap(nextPosition)
621
+ if (changeMap) {
622
+ return true
623
+ }
624
+ }
625
+
626
+ const playerSizeBox = this.getSizeMaxShape(nextPosition.x, nextPosition.y)
627
+
628
+ if (await this.collisionObjects(playerSizeBox, hitbox, options)) {
629
+ if (!options.allSearch) return true
630
+ else collided = true
631
+ }
632
+
633
+ if (await this.collisionShapes(playerSizeBox, nextPosition, options)) {
634
+ if (!options.allSearch) return true
635
+ else collided = true
636
+ }
637
+
638
+ // if there is a change of map after a move, the moves are not changed
639
+ if (prevMapId != this.map) {
640
+ return true
641
+ }
642
+
643
+ return collided
644
+ }
645
+
646
+ /**
647
+ * Attach a shape to the player (and allow interaction with it)
648
+ *
649
+ * ```ts
650
+ * import { ShapePositioning } from '@rpgjs/server'
651
+ *
652
+ * player.attachShape({
653
+ * width: 100,
654
+ * height: 100,
655
+ * positioning: ShapePositioning.Center
656
+ * })
657
+ * ```
658
+ *
659
+ * @title Attach Shape
660
+ * @method player.attachShape(parameters)
661
+ * @param { { width: number, height: number, positioning?, name?, properties?: object } } obj
662
+ * - positioning: Indicate where the shape is placed.
663
+ * - properties: An object in order to retrieve information when interacting with the shape
664
+ * - name: The name of the shape
665
+ * @since 3.0.0-beta.3
666
+ * @returns {RpgShape}
667
+ * @memberof Player
668
+ */
669
+ attachShape(obj: {
670
+ width: number,
671
+ height: number
672
+ positioning?: string
673
+ name?: string
674
+ properties?: object
675
+ }): RpgShape {
676
+ obj.name = (obj.name || generateUID()) as string
677
+ const shape = new RpgShape({
678
+ ...obj,
679
+ fixEvent: this
680
+ } as any)
681
+ this.shapes.push(shape)
682
+ return shape
683
+ }
684
+
685
+ /**
686
+ * Returns all shapes assigned to this player
687
+ *
688
+ * @title Get Shapes
689
+ * @method player.getShapes()
690
+ * @returns {RpgShape[]}
691
+ * @since 3.0.0-beta.3
692
+ * @memberof Player
693
+ * @memberof RpgSpriteLogic
694
+ */
695
+ getShapes(): RpgShape[] {
696
+ return this.shapes
697
+ }
698
+
699
+ private autoChangeDirection(nextPosition: Vector2d) {
700
+ const { x, y } = this.position
701
+ const { x: nx, y: ny } = nextPosition
702
+ const diff = Math.abs(x - nx) > Math.abs(y - ny)
703
+ if (diff) {
704
+ if (nx > x) {
705
+ this.changeDirection(Direction.Right)
706
+ }
707
+ else {
708
+ this.changeDirection(Direction.Left)
709
+ }
710
+ }
711
+ else {
712
+ if (ny > y) {
713
+ this.changeDirection(Direction.Down)
714
+ }
715
+ else {
716
+ this.changeDirection(Direction.Up)
717
+ }
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Stops the movement of the player who moves towards his target
723
+ *
724
+ * @title Stop Move To
725
+ * @method player.stopMoveTo()
726
+ * @returns {void}
727
+ * @since 3.2.0
728
+ * @memberof MoveManager
729
+ */
730
+ stopMoveTo() {
731
+ if (this.destroyMove$.closed) return
732
+ this.destroyMove$.next(true)
733
+ this.destroyMove$.unsubscribe()
734
+ }
735
+
736
+ _moveTo(tick$: Observable<Tick>, positionTarget: AbstractObject | RpgShape | PositionXY, options: MoveTo = {}): Observable<Vector2d> {
737
+ let i = 0
738
+ let count = 0
739
+ const lastPositions: Vector2d[] = []
740
+ this.stopMoveTo()
741
+ this.destroyMove$ = new Subject()
742
+ const { infinite, onStuck, onComplete } = options
743
+ const getPosition = (): Vector2d => {
744
+ let pos
745
+ if ('x' in positionTarget) {
746
+ pos = new Vector2d(positionTarget.x, positionTarget.y)
747
+ }
748
+ else {
749
+ pos = positionTarget.position
750
+ }
751
+ return pos
752
+ }
753
+ return tick$
754
+ .pipe(
755
+ takeUntil(this.destroyMove$),
756
+ takeUntil(this._destroy$),
757
+ mergeMap(() => from(this.computeNextPositionByTarget(this.position.copy(), getPosition()))),
758
+ map((position) => {
759
+ this.autoChangeDirection(position)
760
+ return this.position.set(position)
761
+ }),
762
+ tap((position: Vector2d) => {
763
+ lastPositions[i] = position.copy()
764
+ i++
765
+ count++
766
+ if (i >= 3) {
767
+ i = 0
768
+ }
769
+ if (
770
+ lastPositions[2] && lastPositions[0].isEqual(lastPositions[2])
771
+ ) {
772
+ onStuck?.(count)
773
+ }
774
+ else if (this.position.isEqual(getPosition())) {
775
+ onComplete?.()
776
+ if (!infinite) {
777
+ this.stopMoveTo()
778
+ }
779
+ }
780
+ else {
781
+ count = 0
782
+ }
783
+ })
784
+ )
785
+ }
786
+
787
+ /** @internal */
788
+ async move(nextPosition: Vector2d): Promise<boolean> {
789
+ this.autoChangeDirection(nextPosition)
790
+
791
+ const notCollided = !(await this.isCollided(nextPosition))
792
+
793
+ if (notCollided || !this.checkCollision) {
794
+ this.position = nextPosition.copy()
795
+ await RpgPlugin.emit(HookServer.PlayerMove, this)
796
+ }
797
+
798
+ return true
799
+ }
800
+
801
+ /**
802
+ * Retrieves all shapes where the player is located
803
+ *
804
+ * @title Get In-Shapes
805
+ * @method player.getInShapes()
806
+ * @returns {RpgShape[]}
807
+ * @since 3.0.0-beta.3
808
+ * @memberof Player
809
+ */
810
+ getInShapes(): RpgShape[] {
811
+ return Object.values(this.inShapes)
812
+ }
813
+
814
+ /**
815
+ * Get the current direction.
816
+ *
817
+ * ```ts
818
+ * player.getDirection()
819
+ * ```
820
+ *
821
+ * @title Get Direction
822
+ * @method player.getDirection()
823
+ * @returns {Direction | number} direction
824
+ * @memberof Player
825
+ */
826
+ getDirection(direction?: Direction | number): string | number {
827
+ return direction || this.direction
828
+ }
829
+
830
+ /**
831
+ * Changes the player's direction
832
+ *
833
+ * ```ts
834
+ * import { Direction } from '@rpgjs/server'
835
+ *
836
+ * player.changeDirection(Direction.Left)
837
+ * ```
838
+ *
839
+ * @title Change direction
840
+ * @method player.changeDirection(direction)
841
+ * @param {Direction} direction
842
+ * @enum {string}
843
+ *
844
+ * Direction.Left | left
845
+ * Direction.Right | right
846
+ * Direction.Up | up
847
+ * Direction.Down | down
848
+ * @returns {boolean} the direction has changed
849
+ * @memberof Player
850
+ */
851
+ changeDirection(direction: Direction): boolean {
852
+ const dir = +this.getDirection(direction)
853
+ if (dir === undefined) return false
854
+ this.direction = dir
855
+ return true
856
+ }
857
+
858
+ /**
859
+ * Gets the necessary number of pixels to allow the player to cross a tile.
860
+ * This is the ratio between the height or width of the tile and the speed of the player.
861
+ */
862
+ get nbPixelInTile(): any {
863
+ const direction = this.getDirection()
864
+ switch (direction) {
865
+ case Direction.Down:
866
+ case Direction.Up:
867
+ return Math.floor(this.mapInstance.tileHeight / this.speed)
868
+ case Direction.Left:
869
+ case Direction.Right:
870
+ return Math.floor(this.mapInstance.tileWidth / this.speed)
871
+ default:
872
+ return NaN
873
+ }
874
+ }
875
+
876
+ getSizeMaxShape(x?: number, y?: number): Box {
877
+ const _x = x || this.position.x
878
+ const _y = y || this.position.y
879
+ let minX = _x
880
+ let minY = _y
881
+ let maxX = _x + this.wHitbox
882
+ let maxY = _y + this.hHitbox
883
+ const shapes = this.getShapes()
884
+ for (let shape of shapes) {
885
+ if (shape.x < minX) minX = shape.x
886
+ if (shape.y < minY) minY = shape.y
887
+ const shapeMaxX = shape.x + shape.width
888
+ const shapeMaxY = shape.y + shape.height
889
+ if (shapeMaxX > maxX) maxX = shapeMaxX
890
+ if (shapeMaxY > maxY) maxY = shapeMaxY
891
+ }
892
+ return {
893
+ minX,
894
+ minY,
895
+ maxX,
896
+ maxY
897
+ }
898
+ }
899
+
900
+ /** @internal */
901
+ async execMethod(methodName: string, methodData?, instance?) { }
902
+ /** @internal */
903
+ onAction() { }
904
+ /** @internal */
905
+ onPlayerTouch() { }
906
+ }
907
+
908
+ export interface AbstractObject {
909
+ readonly type: string
910
+ through: boolean
911
+ throughOtherPlayer: boolean
912
+ autoChangeMap?(nextPosition: Position): Promise<boolean>
913
+ execMethod(methodName: string, methodData?, instance?)
914
+ changeMap(mapName: string)
915
+ }