@rpgjs/server 5.0.0-alpha.29 → 5.0.0-alpha.30

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 (51) hide show
  1. package/dist/Gui/DialogGui.d.ts +1 -0
  2. package/dist/Gui/GameoverGui.d.ts +23 -0
  3. package/dist/Gui/Gui.d.ts +6 -0
  4. package/dist/Gui/MenuGui.d.ts +22 -3
  5. package/dist/Gui/NotificationGui.d.ts +1 -2
  6. package/dist/Gui/SaveLoadGui.d.ts +13 -0
  7. package/dist/Gui/ShopGui.d.ts +24 -3
  8. package/dist/Gui/TitleGui.d.ts +23 -0
  9. package/dist/Gui/index.d.ts +9 -1
  10. package/dist/Player/GuiManager.d.ts +86 -3
  11. package/dist/Player/Player.d.ts +24 -2
  12. package/dist/RpgServer.d.ts +7 -0
  13. package/dist/RpgServerEngine.d.ts +2 -1
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.js +1621 -336
  16. package/dist/index.js.map +1 -1
  17. package/dist/presets/index.d.ts +0 -9
  18. package/dist/rooms/BaseRoom.d.ts +37 -0
  19. package/dist/rooms/lobby.d.ts +6 -1
  20. package/dist/rooms/map.d.ts +22 -1
  21. package/dist/services/save.d.ts +43 -0
  22. package/dist/storage/index.d.ts +1 -0
  23. package/dist/storage/localStorage.d.ts +23 -0
  24. package/package.json +10 -10
  25. package/src/Gui/DialogGui.ts +12 -2
  26. package/src/Gui/GameoverGui.ts +39 -0
  27. package/src/Gui/Gui.ts +23 -1
  28. package/src/Gui/MenuGui.ts +155 -6
  29. package/src/Gui/NotificationGui.ts +1 -2
  30. package/src/Gui/SaveLoadGui.ts +60 -0
  31. package/src/Gui/ShopGui.ts +145 -16
  32. package/src/Gui/TitleGui.ts +39 -0
  33. package/src/Gui/index.ts +13 -2
  34. package/src/Player/BattleManager.ts +1 -1
  35. package/src/Player/ClassManager.ts +57 -2
  36. package/src/Player/GuiManager.ts +125 -14
  37. package/src/Player/ItemManager.ts +160 -41
  38. package/src/Player/ParameterManager.ts +1 -1
  39. package/src/Player/Player.ts +87 -12
  40. package/src/Player/SkillManager.ts +145 -66
  41. package/src/Player/StateManager.ts +70 -1
  42. package/src/Player/VariableManager.ts +10 -7
  43. package/src/RpgServer.ts +8 -0
  44. package/src/index.ts +5 -2
  45. package/src/presets/index.ts +1 -10
  46. package/src/rooms/BaseRoom.ts +112 -0
  47. package/src/rooms/lobby.ts +13 -6
  48. package/src/rooms/map.ts +31 -4
  49. package/src/services/save.ts +147 -0
  50. package/src/storage/index.ts +1 -0
  51. package/src/storage/localStorage.ts +76 -0
@@ -1,12 +1,3 @@
1
- export declare const MAXHP: string;
2
- export declare const MAXSP: string;
3
- export declare const ATK: string;
4
- export declare const PDEF: string;
5
- export declare const SDEF: string;
6
- export declare const STR: string;
7
- export declare const AGI: string;
8
- export declare const INT: string;
9
- export declare const DEX: string;
10
1
  export declare const MAXHP_CURVE: {
11
2
  start: number;
12
3
  end: number;
@@ -1,3 +1,5 @@
1
+ import { Hooks } from '../../../common/src';
2
+ import { RpgPlayer } from '../Player/Player';
1
3
  /**
2
4
  * Base class for rooms that need database functionality
3
5
  *
@@ -35,6 +37,7 @@ export declare abstract class BaseRoom {
35
37
  * ```
36
38
  */
37
39
  database: import('@signe/reactive').WritableObjectSignal<{}>;
40
+ onStart(): Promise<void>;
38
41
  /**
39
42
  * Add data to the room's database
40
43
  *
@@ -92,4 +95,38 @@ export declare abstract class BaseRoom {
92
95
  * ```
93
96
  */
94
97
  removeInDatabase(id: string): boolean;
98
+ /**
99
+ * Get the hooks system for this map
100
+ *
101
+ * Returns the dependency-injected Hooks instance that allows you to trigger
102
+ * and listen to various game events.
103
+ *
104
+ * @returns The Hooks instance for this map
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // Trigger a custom hook
109
+ * map.hooks.callHooks('custom-event', data).subscribe();
110
+ * ```
111
+ */
112
+ get hooks(): Hooks;
113
+ /**
114
+ * Resolve complex snapshot entries (e.g. inventory items) before load.
115
+ */
116
+ onSessionRestore({ userSnapshot, user }: {
117
+ userSnapshot: any;
118
+ user?: RpgPlayer;
119
+ }): Promise<any>;
120
+ listSaveSlots(player: RpgPlayer, value: {
121
+ requestId: string;
122
+ }): Promise<import('../../../common/src').SaveSlotList>;
123
+ saveSlot(player: RpgPlayer, value: {
124
+ requestId: string;
125
+ index: number;
126
+ meta?: any;
127
+ }): Promise<void>;
128
+ loadSlot(player: RpgPlayer, value: {
129
+ requestId: string;
130
+ index: number;
131
+ }): Promise<void>;
95
132
  }
@@ -5,5 +5,10 @@ export declare class LobbyRoom extends BaseRoom {
5
5
  players: import('@signe/reactive').WritableObjectSignal<{}>;
6
6
  autoSync: boolean;
7
7
  constructor(room: any);
8
- onJoin(player: RpgPlayer, conn: MockConnection): void;
8
+ onJoin(player: RpgPlayer, conn: MockConnection): Promise<void>;
9
+ guiInteraction(player: RpgPlayer, value: {
10
+ guiId: string;
11
+ name: string;
12
+ data: any;
13
+ }): Promise<void>;
9
14
  }
@@ -168,6 +168,7 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
168
168
  private _autoTickEnabled;
169
169
  autoSync: boolean;
170
170
  constructor(room: any);
171
+ onStart(): Promise<void>;
171
172
  /**
172
173
  * Setup collision detection between players, events, and shapes
173
174
  *
@@ -301,6 +302,10 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
301
302
  * ```
302
303
  */
303
304
  get hooks(): Hooks;
305
+ onSessionRestore(payload: {
306
+ userSnapshot: any;
307
+ user?: RpgPlayer;
308
+ }): Promise<any>;
304
309
  /**
305
310
  * Handle GUI interaction from a player
306
311
  *
@@ -316,7 +321,11 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
316
321
  * // The interaction data is sent from the client
317
322
  * ```
318
323
  */
319
- guiInteraction(player: RpgPlayer, value: any): void;
324
+ guiInteraction(player: RpgPlayer, value: {
325
+ guiId: string;
326
+ name: string;
327
+ data: any;
328
+ }): Promise<void>;
320
329
  /**
321
330
  * Handle GUI exit from a player
322
331
  *
@@ -382,6 +391,18 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
382
391
  * ```
383
392
  */
384
393
  onInput(player: RpgPlayer, input: any): Promise<void>;
394
+ saveSlot(player: RpgPlayer, value: {
395
+ requestId: string;
396
+ index: number;
397
+ meta?: any;
398
+ }): Promise<void>;
399
+ loadSlot(player: RpgPlayer, value: {
400
+ requestId: string;
401
+ index: number;
402
+ }): Promise<void>;
403
+ listSaveSlots(player: RpgPlayer, value: {
404
+ requestId: string;
405
+ }): Promise<import('../../../common/src').SaveSlotList>;
385
406
  /**
386
407
  * Update the map configuration and data
387
408
  *
@@ -0,0 +1,43 @@
1
+ import { SaveSlot, SaveSlotList, SaveSlotMeta } from '../../../common/src';
2
+ import { RpgPlayer } from '../Player/Player';
3
+ export declare const SaveStorageToken = "SaveStorageToken";
4
+ export type SaveSlotIndex = number | "auto";
5
+ export interface SaveRequestContext {
6
+ reason?: "manual" | "auto" | "load";
7
+ source?: string;
8
+ }
9
+ export interface AutoSaveStrategy {
10
+ canSave?: (player: RpgPlayer, context?: SaveRequestContext) => boolean;
11
+ canLoad?: (player: RpgPlayer, context?: SaveRequestContext) => boolean;
12
+ shouldAutoSave?: (player: RpgPlayer, context?: SaveRequestContext) => boolean;
13
+ getDefaultSlot?: (player: RpgPlayer, context?: SaveRequestContext) => number | null;
14
+ }
15
+ export interface SaveStorageStrategy {
16
+ list(player: RpgPlayer): Promise<SaveSlotList>;
17
+ get(player: RpgPlayer, index: number): Promise<SaveSlot | null>;
18
+ save(player: RpgPlayer, index: number, snapshot: string, meta: SaveSlotMeta): Promise<void>;
19
+ delete?(player: RpgPlayer, index: number): Promise<void>;
20
+ }
21
+ export declare class InMemorySaveStorageStrategy implements SaveStorageStrategy {
22
+ private slotsByPlayer;
23
+ list(player: RpgPlayer): Promise<SaveSlotList>;
24
+ get(player: RpgPlayer, index: number): Promise<SaveSlot | null>;
25
+ save(player: RpgPlayer, index: number, snapshot: string, meta: SaveSlotMeta): Promise<void>;
26
+ delete(player: RpgPlayer, index: number): Promise<void>;
27
+ private getSlots;
28
+ private stripSnapshots;
29
+ }
30
+ export declare const AutoSaveToken = "AutoSaveToken";
31
+ export declare function resolveSaveStorageStrategy(): SaveStorageStrategy;
32
+ export declare function resolveAutoSaveStrategy(): AutoSaveStrategy;
33
+ export declare function resolveSaveSlot(slot: SaveSlotIndex | undefined, policy: AutoSaveStrategy, player: RpgPlayer, context?: SaveRequestContext): number | null;
34
+ export declare function shouldAutoSave(player: RpgPlayer, context?: SaveRequestContext): boolean;
35
+ export declare function buildSaveSlotMeta(player: RpgPlayer, overrides?: SaveSlotMeta): SaveSlotMeta;
36
+ export declare function provideSaveStorage(strategy: SaveStorageStrategy): {
37
+ provide: string;
38
+ useValue: SaveStorageStrategy;
39
+ };
40
+ export declare function provideAutoSave(strategy: AutoSaveStrategy): {
41
+ provide: string;
42
+ useValue: AutoSaveStrategy;
43
+ };
@@ -0,0 +1 @@
1
+ export * from './localStorage';
@@ -0,0 +1,23 @@
1
+ import { SaveSlot, SaveSlotList, SaveSlotMeta } from '../../../common/src';
2
+ import { SaveStorageStrategy } from '../services/save';
3
+ import { RpgPlayer } from '../Player/Player';
4
+ export interface LocalStorageSaveStorageOptions {
5
+ key?: string;
6
+ }
7
+ /**
8
+ * Save storage strategy backed by browser localStorage.
9
+ *
10
+ * Intended for standalone mode where the server runs in the browser
11
+ * and localStorage is available.
12
+ */
13
+ export declare class LocalStorageSaveStorageStrategy implements SaveStorageStrategy {
14
+ private key;
15
+ constructor(options?: LocalStorageSaveStorageOptions);
16
+ list(_player: RpgPlayer): Promise<SaveSlotList>;
17
+ get(_player: RpgPlayer, index: number): Promise<SaveSlot | null>;
18
+ save(_player: RpgPlayer, index: number, snapshot: string, meta: SaveSlotMeta): Promise<void>;
19
+ delete(_player: RpgPlayer, index: number): Promise<void>;
20
+ private readSlots;
21
+ private writeSlots;
22
+ private stripSnapshots;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/server",
3
- "version": "5.0.0-alpha.29",
3
+ "version": "5.0.0-alpha.30",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "publishConfig": {
@@ -11,19 +11,19 @@
11
11
  "license": "MIT",
12
12
  "description": "",
13
13
  "dependencies": {
14
- "@rpgjs/common": "5.0.0-alpha.29",
15
- "@rpgjs/physic": "5.0.0-alpha.29",
16
- "@rpgjs/testing": "5.0.0-alpha.29",
14
+ "@rpgjs/common": "5.0.0-alpha.30",
15
+ "@rpgjs/physic": "5.0.0-alpha.30",
16
+ "@rpgjs/testing": "5.0.0-alpha.30",
17
17
  "@rpgjs/database": "^4.3.0",
18
- "@signe/di": "^2.7.3",
19
- "@signe/reactive": "^2.7.3",
20
- "@signe/room": "^2.7.3",
21
- "@signe/sync": "^2.7.3",
18
+ "@signe/di": "^2.8.2",
19
+ "@signe/reactive": "^2.8.2",
20
+ "@signe/room": "^2.8.2",
21
+ "@signe/sync": "^2.8.2",
22
22
  "rxjs": "^7.8.2",
23
- "zod": "^4.2.1"
23
+ "zod": "^4.3.5"
24
24
  },
25
25
  "devDependencies": {
26
- "vite": "^7.3.0",
26
+ "vite": "^7.3.1",
27
27
  "vite-plugin-dts": "^4.5.4"
28
28
  },
29
29
  "type": "module",
@@ -19,6 +19,7 @@ export interface DialogOptions {
19
19
  tranparent?: boolean,
20
20
  typewriterEffect?: boolean,
21
21
  talkWith?: RpgPlayer,
22
+ speaker?: string,
22
23
  face?: {
23
24
  id: string,
24
25
  expression: string
@@ -34,9 +35,17 @@ export class DialogGui extends Gui {
34
35
  if (!options.choices) options.choices = []
35
36
  if (options.autoClose == undefined) options.autoClose = false
36
37
  if (!options.position) options.position = DialogPosition.Bottom
37
- if (options.fullWidth == undefined) options.fullWidth = true
38
+ if (options.fullWidth == undefined) options.fullWidth = false
38
39
  if (options.typewriterEffect == undefined) options.typewriterEffect = true
39
40
  const event = options.talkWith
41
+ const resolveName = (target?: RpgPlayer): string | undefined => {
42
+ if (!target) return undefined
43
+ const rawName = (target as any).name
44
+ if (typeof rawName === 'function') return rawName()
45
+ if (rawName && typeof rawName.get === 'function') return rawName.get()
46
+ return rawName
47
+ }
48
+ const speaker = options.speaker ?? resolveName(event)
40
49
  let memoryDir
41
50
  if (event) {
42
51
  memoryDir = event.direction()
@@ -48,6 +57,7 @@ export class DialogGui extends Gui {
48
57
  position: options.position,
49
58
  fullWidth: options.fullWidth,
50
59
  typewriterEffect: options.typewriterEffect,
60
+ speaker,
51
61
  // remove value property. It is not useful to know this on the client side.
52
62
  choices: options.choices.map(choice => ({
53
63
  text: choice.text
@@ -68,4 +78,4 @@ export class DialogGui extends Gui {
68
78
  return val
69
79
  })
70
80
  }
71
- }
81
+ }
@@ -0,0 +1,39 @@
1
+ import { PrebuiltGui } from '@rpgjs/common'
2
+ import { Gui } from './Gui'
3
+ import { RpgPlayer } from '../Player/Player'
4
+
5
+ export interface GameoverEntry {
6
+ id: string
7
+ label: string
8
+ disabled?: boolean
9
+ }
10
+
11
+ export interface GameoverGuiOptions {
12
+ entries?: GameoverEntry[]
13
+ title?: string
14
+ subtitle?: string
15
+ saveLoad?: Record<string, any>
16
+ localActions?: boolean
17
+ }
18
+
19
+ export interface GameoverGuiSelection {
20
+ id?: string
21
+ index?: number
22
+ entry?: GameoverEntry
23
+ }
24
+
25
+ export class GameoverGui extends Gui {
26
+ constructor(player: RpgPlayer) {
27
+ super(PrebuiltGui.Gameover, player)
28
+ }
29
+
30
+ open(options: GameoverGuiOptions = {}): Promise<GameoverGuiSelection | null> {
31
+ this.on('select', (selection: GameoverGuiSelection) => {
32
+ this.close(selection)
33
+ })
34
+ return super.open(options, {
35
+ waitingAction: true,
36
+ blockPlayerInput: true
37
+ })
38
+ }
39
+ }
package/src/Gui/Gui.ts CHANGED
@@ -4,6 +4,7 @@ export class Gui {
4
4
 
5
5
  private _close: Function = () => {}
6
6
  private _blockPlayerInput: boolean = false
7
+ private _events = new Map<string, (data: any) => void>()
7
8
 
8
9
  constructor(
9
10
  public id: string,
@@ -34,6 +35,19 @@ export class Gui {
34
35
  })
35
36
  }
36
37
 
38
+ on(event: string, callback: (data: any) => void) {
39
+ this._events.set(event, callback)
40
+ }
41
+
42
+ async emit(event: string, data: any): Promise<any> {
43
+ const callback = this._events.get(event)
44
+ if (callback) {
45
+ return await callback(data)
46
+ } else {
47
+ return null
48
+ }
49
+ }
50
+
37
51
  close(data?) {
38
52
  this.player.emit('gui.exit', this.id)
39
53
  if (this._blockPlayerInput) {
@@ -41,4 +55,12 @@ export class Gui {
41
55
  }
42
56
  this._close(data)
43
57
  }
44
- }
58
+
59
+ update(data?, { clientActionId }: { clientActionId?: string } = {}) {
60
+ this.player.emit('gui.update', {
61
+ guiId: this.id,
62
+ data,
63
+ clientActionId
64
+ })
65
+ }
66
+ }
@@ -1,15 +1,137 @@
1
1
  import { PrebuiltGui } from '@rpgjs/common'
2
2
  import { Gui } from './Gui'
3
3
  import { RpgPlayer } from '../Player/Player'
4
- import { IGui } from '../Interfaces/Gui'
4
+ import { SaveLoadGui, SaveSlot } from './SaveLoadGui'
5
+ import { resolveAutoSaveStrategy } from '../services/save'
6
+
7
+ export type MenuEntryId = 'items' | 'skills' | 'equip' | 'options' | 'save' | 'exit'
8
+
9
+ export interface MenuEntry {
10
+ id: MenuEntryId
11
+ label: string
12
+ disabled?: boolean
13
+ }
14
+
15
+ export interface MenuGuiOptions {
16
+ menus?: MenuEntry[]
17
+ disabled?: MenuEntryId[]
18
+ saveSlots?: SaveSlot[]
19
+ saveMaxSlots?: number
20
+ saveShowAutoSlot?: boolean
21
+ saveAutoSlotIndex?: number
22
+ saveAutoSlotLabel?: string
23
+ }
24
+
25
+ export class MenuGui extends Gui {
26
+ private menuOptions: MenuGuiOptions = {}
5
27
 
6
- export class MenuGui extends Gui implements IGui {
7
28
  constructor(player: RpgPlayer) {
8
29
  super(PrebuiltGui.MainMenu, player)
9
30
  }
10
31
 
11
- open() {
12
- this.on('useItem', (id) => {
32
+ private buildSaveLoad(options: MenuGuiOptions) {
33
+ const autoSave = resolveAutoSaveStrategy()
34
+ const canSave = autoSave.canSave ? autoSave.canSave(this.player, { reason: "manual", source: "menu" }) : true
35
+ const autoSlotIndex = options.saveAutoSlotIndex ?? autoSave.getDefaultSlot?.(this.player, { reason: "auto", source: "menu" }) ?? 0
36
+ return {
37
+ mode: 'save',
38
+ canSave,
39
+ showAutoSlot: options.saveShowAutoSlot === true,
40
+ autoSlotIndex,
41
+ autoSlotLabel: options.saveAutoSlotLabel
42
+ }
43
+ }
44
+
45
+ private buildMenuData(options: MenuGuiOptions) {
46
+ const disabledSet = new Set(options.disabled || [])
47
+ const defaultMenus: MenuEntry[] = [
48
+ { id: 'items', label: 'Items' },
49
+ { id: 'skills', label: 'Skills' },
50
+ { id: 'equip', label: 'Equip' },
51
+ { id: 'options', label: 'Options' },
52
+ { id: 'save', label: 'Save' },
53
+ { id: 'exit', label: 'Exit' }
54
+ ]
55
+ const menus = (options.menus && options.menus.length ? options.menus : defaultMenus)
56
+ .map(menu => ({
57
+ ...menu,
58
+ disabled: menu.disabled || disabledSet.has(menu.id)
59
+ }))
60
+
61
+ const player = this.player as any
62
+ const databaseById = player.databaseById?.bind(player)
63
+ const equippedIds = new Set(
64
+ (player.equipments?.() || []).map((it) => it?.id?.() ?? it?.id ?? it?.name)
65
+ )
66
+
67
+ const buildStats = () => {
68
+ const params = player.param || {}
69
+ const statKeys = [
70
+ 'str',
71
+ 'dex',
72
+ 'int',
73
+ 'agi',
74
+ 'maxHp',
75
+ 'maxSp'
76
+ ]
77
+ const stats: Record<string, number> = {}
78
+ statKeys.forEach((key) => {
79
+ stats[key] = params[key] ?? 0
80
+ })
81
+ stats.pdef = player.pdef ?? params.pdef ?? 0
82
+ stats.sdef = player.sdef ?? params.sdef ?? 0
83
+ stats.atk = player.atk ?? params.atk ?? 0
84
+ return stats
85
+ }
86
+
87
+ const items = (player.items?.() || []).map((item) => {
88
+ const id = item.id()
89
+ const data = databaseById ? databaseById(id) : {}
90
+ const type = data?._type ?? 'item'
91
+ const consumable = data?.consumable
92
+ const isConsumable = consumable !== undefined ? consumable : type === 'item'
93
+ const usable = isConsumable === false
94
+ ? false
95
+ : consumable === undefined && type !== 'item'
96
+ ? false
97
+ : true
98
+ return {
99
+ id,
100
+ name: item.name(),
101
+ description: item.description(),
102
+ quantity: item.quantity(),
103
+ icon: data?.icon ?? (item as any)?.icon,
104
+ atk: item.atk(),
105
+ pdef: item.pdef(),
106
+ sdef: item.sdef(),
107
+ consumable: isConsumable,
108
+ type,
109
+ usable,
110
+ equipped: equippedIds.has(id)
111
+ }
112
+ })
113
+ const menuEquips = items.filter((item) => item.type === 'weapon' || item.type === 'armor')
114
+ const skills = (player.skills?.() || []).map((skill) => ({
115
+ id: skill?.id() ?? skill?.name(),
116
+ name: skill?.name() ?? skill?.id() ?? 'Skill',
117
+ description: skill?.description() ?? '',
118
+ spCost: skill?.spCost() ?? 0
119
+ }))
120
+ const saveLoad = this.buildSaveLoad(options)
121
+
122
+ return { menus, items, equips: menuEquips, skills, saveLoad, playerStats: buildStats() }
123
+ }
124
+
125
+ private refreshMenu(clientActionId?: string) {
126
+ const data = this.buildMenuData(this.menuOptions)
127
+ this.update(data, { clientActionId })
128
+ }
129
+
130
+ open(options: MenuGuiOptions = {}) {
131
+ this.menuOptions = options
132
+ const data = this.buildMenuData(options)
133
+
134
+ this.on('useItem', ({ id, clientActionId }) => {
13
135
  try {
14
136
  this.player.useItem(id)
15
137
  this.player.syncChanges()
@@ -17,10 +139,37 @@ export class MenuGui extends Gui implements IGui {
17
139
  catch (err: any) {
18
140
  this.player.showNotification(err.msg)
19
141
  }
142
+ finally {
143
+ this.refreshMenu(clientActionId)
144
+ }
145
+ })
146
+ this.on('equipItem', ({ id, equip, clientActionId }) => {
147
+ try {
148
+ this.player.equip(id, equip)
149
+ this.player.syncChanges()
150
+ }
151
+ catch (err: any) {
152
+ this.player.showNotification(err.msg)
153
+ }
154
+ finally {
155
+ this.refreshMenu(clientActionId)
156
+ }
157
+ })
158
+ this.on('openSave', async () => {
159
+ this.close()
160
+ const gui = new SaveLoadGui(this.player)
161
+ player._gui[gui.id] = gui
162
+ await gui.open(options.saveSlots || [], {
163
+ mode: 'save',
164
+ maxSlots: options.saveMaxSlots
165
+ })
166
+ })
167
+ this.on('exit', () => {
168
+ this.close('exit')
20
169
  })
21
- return super.open('', {
170
+ return super.open(data, {
22
171
  waitingAction: true,
23
172
  blockPlayerInput: true
24
173
  })
25
174
  }
26
- }
175
+ }
@@ -1,9 +1,8 @@
1
1
  import { PrebuiltGui } from '@rpgjs/common'
2
2
  import { Gui } from './Gui'
3
3
  import { RpgPlayer } from '../Player/Player'
4
- import { IGui } from '../Interfaces/Gui'
5
4
 
6
- export class NotificationGui extends Gui implements IGui {
5
+ export class NotificationGui extends Gui {
7
6
  constructor(player: RpgPlayer) {
8
7
  super(PrebuiltGui.Notification, player)
9
8
  }
@@ -0,0 +1,60 @@
1
+ import { PrebuiltGui } from '@rpgjs/common'
2
+ import type { SaveSlot } from '@rpgjs/common'
3
+ import { Gui } from './Gui'
4
+ import { RpgPlayer } from '../Player/Player'
5
+
6
+ export type SaveLoadMode = 'save' | 'load'
7
+
8
+ export type { SaveSlot } from '@rpgjs/common'
9
+
10
+ export interface SaveLoadOptions {
11
+ mode?: SaveLoadMode
12
+ maxSlots?: number
13
+ }
14
+
15
+ export class SaveLoadGui extends Gui {
16
+ constructor(player: RpgPlayer) {
17
+ super(PrebuiltGui.Save, player)
18
+ }
19
+
20
+ open(slots: SaveSlot[] = [], options: SaveLoadOptions = {}): Promise<number | null> {
21
+ const mode = options.mode || 'load'
22
+ const maxSlots = options.maxSlots ?? slots.length
23
+ const normalizedSlots = Array.from({ length: maxSlots }, (_, index) => slots[index] ?? null)
24
+ const uiSlots = normalizedSlots.map((slot) => {
25
+ if (!slot) return null
26
+ const { snapshot, ...data } = slot
27
+ return data
28
+ })
29
+
30
+ const onSelect = async ({ index }) => {
31
+ if (typeof index !== 'number') return
32
+ if (index < 0 || index >= normalizedSlots.length) return
33
+ const slot = normalizedSlots[index]
34
+ if (mode === 'load') {
35
+ const result = await this.player.load(index, { reason: "load", source: "gui" }, { changeMap: true })
36
+ if (!result.ok) return
37
+ this.close(index)
38
+ return
39
+ }
40
+ if (mode === 'save') {
41
+ const result = await this.player.save(index, {}, { reason: "manual", source: "gui" })
42
+ if (!result) return
43
+ const updatedSlot: SaveSlot = {
44
+ ...(slot || {}),
45
+ ...result.meta
46
+ }
47
+ normalizedSlots[index] = updatedSlot
48
+ slots[index] = updatedSlot
49
+ this.close(index)
50
+ }
51
+ }
52
+ this.on('save', onSelect)
53
+ this.on('load', onSelect)
54
+ this.on('select', onSelect)
55
+ return super.open({ slots: uiSlots, mode }, {
56
+ waitingAction: true,
57
+ blockPlayerInput: true
58
+ })
59
+ }
60
+ }