@rpgjs/server 5.0.0-alpha.4 → 5.0.0-alpha.41

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 (101) hide show
  1. package/dist/Gui/DialogGui.d.ts +5 -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 +28 -3
  8. package/dist/Gui/TitleGui.d.ts +23 -0
  9. package/dist/Gui/index.d.ts +10 -1
  10. package/dist/Player/BattleManager.d.ts +34 -12
  11. package/dist/Player/ClassManager.d.ts +46 -13
  12. package/dist/Player/ComponentManager.d.ts +123 -0
  13. package/dist/Player/Components.d.ts +345 -0
  14. package/dist/Player/EffectManager.d.ts +86 -0
  15. package/dist/Player/ElementManager.d.ts +104 -0
  16. package/dist/Player/GoldManager.d.ts +22 -0
  17. package/dist/Player/GuiManager.d.ts +259 -0
  18. package/dist/Player/ItemFixture.d.ts +6 -0
  19. package/dist/Player/ItemManager.d.ts +450 -9
  20. package/dist/Player/MoveManager.d.ts +324 -69
  21. package/dist/Player/ParameterManager.d.ts +344 -14
  22. package/dist/Player/Player.d.ts +460 -8
  23. package/dist/Player/SkillManager.d.ts +197 -15
  24. package/dist/Player/StateManager.d.ts +89 -25
  25. package/dist/Player/VariableManager.d.ts +74 -0
  26. package/dist/RpgServer.d.ts +502 -64
  27. package/dist/RpgServerEngine.d.ts +2 -1
  28. package/dist/decorators/event.d.ts +46 -0
  29. package/dist/decorators/map.d.ts +287 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +21653 -20900
  32. package/dist/index.js.map +1 -1
  33. package/dist/logs/log.d.ts +2 -3
  34. package/dist/module.d.ts +43 -1
  35. package/dist/presets/index.d.ts +0 -9
  36. package/dist/rooms/BaseRoom.d.ts +132 -0
  37. package/dist/rooms/lobby.d.ts +10 -2
  38. package/dist/rooms/map.d.ts +1236 -17
  39. package/dist/services/save.d.ts +43 -0
  40. package/dist/storage/index.d.ts +1 -0
  41. package/dist/storage/localStorage.d.ts +23 -0
  42. package/package.json +14 -10
  43. package/src/Gui/DialogGui.ts +19 -4
  44. package/src/Gui/GameoverGui.ts +39 -0
  45. package/src/Gui/Gui.ts +23 -1
  46. package/src/Gui/MenuGui.ts +155 -6
  47. package/src/Gui/NotificationGui.ts +1 -2
  48. package/src/Gui/SaveLoadGui.ts +60 -0
  49. package/src/Gui/ShopGui.ts +146 -16
  50. package/src/Gui/TitleGui.ts +39 -0
  51. package/src/Gui/index.ts +15 -2
  52. package/src/Player/BattleManager.ts +91 -49
  53. package/src/Player/ClassManager.ts +118 -50
  54. package/src/Player/ComponentManager.ts +425 -19
  55. package/src/Player/Components.ts +380 -0
  56. package/src/Player/EffectManager.ts +81 -44
  57. package/src/Player/ElementManager.ts +109 -86
  58. package/src/Player/GoldManager.ts +32 -35
  59. package/src/Player/GuiManager.ts +308 -150
  60. package/src/Player/ItemFixture.ts +4 -5
  61. package/src/Player/ItemManager.ts +774 -355
  62. package/src/Player/MoveManager.ts +1544 -774
  63. package/src/Player/ParameterManager.ts +546 -104
  64. package/src/Player/Player.ts +1163 -88
  65. package/src/Player/SkillManager.ts +520 -195
  66. package/src/Player/StateManager.ts +170 -182
  67. package/src/Player/VariableManager.ts +101 -63
  68. package/src/RpgServer.ts +525 -63
  69. package/src/core/context.ts +1 -0
  70. package/src/decorators/event.ts +61 -0
  71. package/src/decorators/map.ts +327 -0
  72. package/src/index.ts +11 -1
  73. package/src/logs/log.ts +10 -3
  74. package/src/module.ts +126 -3
  75. package/src/presets/index.ts +1 -10
  76. package/src/rooms/BaseRoom.ts +232 -0
  77. package/src/rooms/lobby.ts +25 -7
  78. package/src/rooms/map.ts +2502 -194
  79. package/src/services/save.ts +147 -0
  80. package/src/storage/index.ts +1 -0
  81. package/src/storage/localStorage.ts +76 -0
  82. package/tests/battle.spec.ts +375 -0
  83. package/tests/change-map.spec.ts +72 -0
  84. package/tests/class.spec.ts +274 -0
  85. package/tests/effect.spec.ts +219 -0
  86. package/tests/element.spec.ts +221 -0
  87. package/tests/event.spec.ts +80 -0
  88. package/tests/gold.spec.ts +99 -0
  89. package/tests/item.spec.ts +609 -0
  90. package/tests/module.spec.ts +38 -0
  91. package/tests/move.spec.ts +601 -0
  92. package/tests/player-param.spec.ts +28 -0
  93. package/tests/prediction-reconciliation.spec.ts +182 -0
  94. package/tests/random-move.spec.ts +65 -0
  95. package/tests/skill.spec.ts +658 -0
  96. package/tests/state.spec.ts +467 -0
  97. package/tests/variable.spec.ts +185 -0
  98. package/tests/world-maps.spec.ts +896 -0
  99. package/vite.config.ts +16 -0
  100. package/dist/Player/Event.d.ts +0 -0
  101. package/src/Player/Event.ts +0 -0
@@ -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.4",
3
+ "version": "5.0.0-alpha.41",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "publishConfig": {
@@ -11,21 +11,25 @@
11
11
  "license": "MIT",
12
12
  "description": "",
13
13
  "dependencies": {
14
- "@rpgjs/common": "5.0.0-alpha.4",
14
+ "@rpgjs/common": "5.0.0-alpha.41",
15
+ "@rpgjs/physic": "5.0.0-alpha.41",
16
+ "@rpgjs/testing": "5.0.0-alpha.41",
15
17
  "@rpgjs/database": "^4.3.0",
16
- "@signe/di": "^2.3.2",
17
- "@signe/reactive": "^2.3.2",
18
- "@signe/room": "^2.3.2",
19
- "@signe/sync": "^2.3.2",
20
- "rxjs": "^7.8.2"
18
+ "@signe/di": "^2.8.3",
19
+ "@signe/reactive": "^2.8.3",
20
+ "@signe/room": "^2.8.3",
21
+ "@signe/sync": "^2.8.3",
22
+ "rxjs": "^7.8.2",
23
+ "zod": "^4.3.6"
21
24
  },
22
25
  "devDependencies": {
23
- "vite": "^6.2.5",
24
- "vite-plugin-dts": "^4.5.3"
26
+ "vite": "^7.3.1",
27
+ "vite-plugin-dts": "^4.5.4"
25
28
  },
26
29
  "type": "module",
27
30
  "scripts": {
28
31
  "dev": "vite build --watch",
29
- "build": "vite build"
32
+ "build": "vite build",
33
+ "test": "vitest"
30
34
  }
31
35
  }
@@ -18,7 +18,12 @@ export interface DialogOptions {
18
18
  autoClose?: boolean,
19
19
  tranparent?: boolean,
20
20
  typewriterEffect?: boolean,
21
- talkWith?: RpgPlayer
21
+ talkWith?: RpgPlayer,
22
+ speaker?: string,
23
+ face?: {
24
+ id: string,
25
+ expression: string
26
+ },
22
27
  }
23
28
 
24
29
  export class DialogGui extends Gui {
@@ -30,9 +35,17 @@ export class DialogGui extends Gui {
30
35
  if (!options.choices) options.choices = []
31
36
  if (options.autoClose == undefined) options.autoClose = false
32
37
  if (!options.position) options.position = DialogPosition.Bottom
33
- if (options.fullWidth == undefined) options.fullWidth = true
38
+ if (options.fullWidth == undefined) options.fullWidth = false
34
39
  if (options.typewriterEffect == undefined) options.typewriterEffect = true
35
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)
36
49
  let memoryDir
37
50
  if (event) {
38
51
  memoryDir = event.direction()
@@ -44,10 +57,12 @@ export class DialogGui extends Gui {
44
57
  position: options.position,
45
58
  fullWidth: options.fullWidth,
46
59
  typewriterEffect: options.typewriterEffect,
60
+ speaker,
47
61
  // remove value property. It is not useful to know this on the client side.
48
62
  choices: options.choices.map(choice => ({
49
63
  text: choice.text
50
- }))
64
+ })),
65
+ face: options.face
51
66
  }
52
67
  return super.open({
53
68
  message,
@@ -63,4 +78,4 @@ export class DialogGui extends Gui {
63
78
  return val
64
79
  })
65
80
  }
66
- }
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(), expForNextlevel: player.expForNextlevel }
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
+ }