@rpgjs/server 5.0.0-alpha.42 → 5.0.0-alpha.44

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.
@@ -176,6 +176,48 @@ export interface SkillObject extends SkillHooks {
176
176
  */
177
177
  export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
178
178
  return class extends (Base as any) {
179
+ private _getSkillSnapshot(skillData: any) {
180
+ if (!skillData) return null;
181
+
182
+ const snapshot = {
183
+ ...((skillData as any)._skillData || skillData),
184
+ };
185
+
186
+ const reactiveKeys = [
187
+ "id",
188
+ "name",
189
+ "description",
190
+ "spCost",
191
+ "icon",
192
+ "hitRate",
193
+ "power",
194
+ "coefficient",
195
+ ];
196
+
197
+ for (const key of reactiveKeys) {
198
+ const value = (skillData as any)[key];
199
+ if (typeof value === "function") {
200
+ if (key === "hitRate" && !(key in snapshot)) {
201
+ continue;
202
+ }
203
+ snapshot[key] = value();
204
+ } else if (value !== undefined) {
205
+ snapshot[key] = value;
206
+ }
207
+ }
208
+
209
+ if ((skillData as any)._skillInstance) {
210
+ snapshot._skillInstance = (skillData as any)._skillInstance;
211
+ }
212
+
213
+ return snapshot;
214
+ }
215
+
216
+ private _getLearnedSkillEntry(skillInput: SkillClass | SkillObject | string): Skill | null {
217
+ const index = this._getSkillIndex(skillInput);
218
+ return index >= 0 ? ((this as any).skills()[index] as Skill) : null;
219
+ }
220
+
179
221
  private _getSkillMap(required: boolean = true) {
180
222
  // Use this.map directly to support both RpgMap and LobbyRoom
181
223
  const map = (this as any).getCurrentMap?.() || (this as any).map;
@@ -246,6 +288,10 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
246
288
  ) {
247
289
  const instance = new Skill(skillData);
248
290
  instance.id.set(skillId);
291
+ (instance as any)._skillData = {
292
+ ...skillData,
293
+ id: skillId,
294
+ };
249
295
 
250
296
  if (skillInstance) {
251
297
  (instance as any)._skillInstance = skillInstance;
@@ -349,8 +395,8 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
349
395
  * ```
350
396
  */
351
397
  getSkill(skillInput: SkillClass | SkillObject | string): Skill | null {
352
- const index = this._getSkillIndex(skillInput);
353
- return (this as any).skills()[index] as Skill ?? null;
398
+ const skill = this._getLearnedSkillEntry(skillInput);
399
+ return this._getSkillSnapshot(skill) as Skill | null;
354
400
  }
355
401
 
356
402
  /**
@@ -389,7 +435,7 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
389
435
  const { skillId, skillData, skillInstance } = this._resolveSkillInput(skillInput, map);
390
436
 
391
437
  // Check if already learned
392
- if (this.getSkill(skillId)) {
438
+ if (this._getLearnedSkillEntry(skillId)) {
393
439
  throw SkillLog.alreadyLearned(skillData);
394
440
  }
395
441
 
@@ -438,11 +484,15 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
438
484
  throw SkillLog.notLearned(skillData);
439
485
  }
440
486
 
441
- const skillData = (this as any).skills()[index];
487
+ const skillEntry = (this as any).skills()[index];
488
+ const skillData = this._getSkillSnapshot(skillEntry);
442
489
  (this as any).skills().splice(index, 1);
443
490
 
444
491
  // Call onForget hook
445
- const hookTarget = (skillData as any)?._skillInstance || skillData;
492
+ const hookTarget =
493
+ (skillEntry as any)?._skillInstance ||
494
+ (skillEntry as any)?._skillData ||
495
+ skillData;
446
496
  this["execMethod"]("onForget", [this], hookTarget);
447
497
 
448
498
  return skillData;
@@ -475,7 +525,8 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
475
525
  * ```
476
526
  */
477
527
  useSkill(skillInput: SkillClass | SkillObject | string, otherPlayer?: RpgPlayer | RpgPlayer[]): any {
478
- const skill = this.getSkill(skillInput);
528
+ const skillEntry = this._getLearnedSkillEntry(skillInput);
529
+ const skill = this._getSkillSnapshot(skillEntry);
479
530
 
480
531
  // Check for skill restriction effect
481
532
  if ((this as any).hasEffect(Effect.CAN_NOT_SKILL)) {
@@ -488,7 +539,7 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
488
539
  }
489
540
 
490
541
  // Check SP cost
491
- const spCost = skill.spCost || 0;
542
+ const spCost = typeof skill.spCost === "number" ? skill.spCost : 0;
492
543
  if (spCost > (this as any).sp) {
493
544
  throw SkillLog.notEnoughSp(skill, spCost, (this as any).sp);
494
545
  }
@@ -498,9 +549,12 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
498
549
  (this as any).sp -= spCost / costMultiplier;
499
550
 
500
551
  // Check hit rate
501
- const hitRate = skill.hitRate ?? 1;
552
+ const hitRate = typeof skill.hitRate === "number" ? skill.hitRate : 1;
502
553
  if (Math.random() > hitRate) {
503
- const hookTarget = (skill as any)?._skillInstance || skill;
554
+ const hookTarget =
555
+ (skillEntry as any)?._skillInstance ||
556
+ (skillEntry as any)?._skillData ||
557
+ skill;
504
558
  this["execMethod"]("onUseFailed", [this, otherPlayer], hookTarget);
505
559
  throw SkillLog.chanceToUseFailed(skill);
506
560
  }
@@ -515,7 +569,10 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
515
569
  }
516
570
 
517
571
  // Call onUse hook
518
- const hookTarget = (skill as any)?._skillInstance || skill;
572
+ const hookTarget =
573
+ (skillEntry as any)?._skillInstance ||
574
+ (skillEntry as any)?._skillData ||
575
+ skill;
519
576
  this["execMethod"]("onUse", [this, otherPlayer], hookTarget);
520
577
 
521
578
  return skill;
@@ -30,7 +30,9 @@ export function WithVariableManager<TBase extends PlayerCtor>(Base: TBase) {
30
30
  variables = type(signal<Record<string, any>>({}) as any, 'variables', { persist: true }, this as any);
31
31
 
32
32
  setVariable(key: string, val: any): void {
33
- this.variables()[key] = val;
33
+ this.variables.mutate((variables) => {
34
+ variables[key] = val;
35
+ });
34
36
  }
35
37
 
36
38
  getVariable<U = any>(key: string): U | undefined {
@@ -38,7 +40,13 @@ export function WithVariableManager<TBase extends PlayerCtor>(Base: TBase) {
38
40
  }
39
41
 
40
42
  removeVariable(key: string): boolean {
41
- delete this.variables()[key];
43
+ const variables = this.variables();
44
+ if (!(key in variables)) {
45
+ return false;
46
+ }
47
+ this.variables.mutate((draft) => {
48
+ delete draft[key];
49
+ });
42
50
  return true;
43
51
  }
44
52
 
@@ -110,4 +118,4 @@ export interface IVariableManager {
110
118
  * Clear all variables
111
119
  */
112
120
  clearVariables(): void;
113
- }
121
+ }
@@ -0,0 +1,254 @@
1
+ import type { RpgWebSocketConnection } from "./types";
2
+
3
+ type RuntimeProcess = {
4
+ env?: Record<string, string | undefined>;
5
+ };
6
+
7
+ function readEnvVariable(name: string): string | undefined {
8
+ const value = (globalThis as { process?: RuntimeProcess }).process?.env?.[name];
9
+ return typeof value === "string" ? value : undefined;
10
+ }
11
+
12
+ export class PartyConnection {
13
+ public id: string;
14
+ public uri: string;
15
+ private _state: any = {};
16
+ private messageQueue: Array<{ message: string; timestamp: number; sequence: number }> = [];
17
+ private isProcessingQueue = false;
18
+ private sequenceCounter = 0;
19
+ private incomingQueue: Array<{
20
+ message: string;
21
+ timestamp: number;
22
+ processor: (messages: string[]) => Promise<void>;
23
+ }> = [];
24
+ private isProcessingIncomingQueue = false;
25
+
26
+ public static packetLossRate = parseFloat(readEnvVariable("RPGJS_PACKET_LOSS_RATE") || "0.1");
27
+ public static packetLossEnabled = readEnvVariable("RPGJS_ENABLE_PACKET_LOSS") === "true";
28
+ public static packetLossFilter = readEnvVariable("RPGJS_PACKET_LOSS_FILTER") || "";
29
+ public static bandwidthEnabled = readEnvVariable("RPGJS_ENABLE_BANDWIDTH") === "true";
30
+ public static bandwidthKbps = parseInt(readEnvVariable("RPGJS_BANDWIDTH_KBPS") || "100");
31
+ public static bandwidthFilter = readEnvVariable("RPGJS_BANDWIDTH_FILTER") || "";
32
+ public static latencyEnabled = readEnvVariable("RPGJS_ENABLE_LATENCY") === "true";
33
+ public static latencyMs = parseInt(readEnvVariable("RPGJS_LATENCY_MS") || "50");
34
+ public static latencyFilter = readEnvVariable("RPGJS_LATENCY_FILTER") || "";
35
+
36
+ constructor(private ws: RpgWebSocketConnection, id?: string, uri?: string) {
37
+ this.id = id || this.generateId();
38
+ this.uri = uri || "";
39
+ }
40
+
41
+ private generateId(): string {
42
+ return `conn_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
43
+ }
44
+
45
+ async send(data: any): Promise<void> {
46
+ if (this.ws.readyState !== 1) {
47
+ return;
48
+ }
49
+
50
+ const message = typeof data === "string" ? data : JSON.stringify(data);
51
+ const timestamp = Date.now();
52
+ const sequence = ++this.sequenceCounter;
53
+
54
+ this.messageQueue.push({ message, timestamp, sequence });
55
+
56
+ if (!this.isProcessingQueue) {
57
+ void this.processMessageQueue();
58
+ }
59
+ }
60
+
61
+ private async processMessageQueue(): Promise<void> {
62
+ if (this.isProcessingQueue) {
63
+ return;
64
+ }
65
+ this.isProcessingQueue = true;
66
+
67
+ while (this.messageQueue.length > 0) {
68
+ const queueItem = this.messageQueue.shift()!;
69
+
70
+ if (this.shouldApplyLatency(queueItem.message)) {
71
+ await this.waitUntil(queueItem.timestamp + PartyConnection.latencyMs);
72
+ }
73
+
74
+ if (PartyConnection.bandwidthEnabled && PartyConnection.bandwidthKbps > 0) {
75
+ if (!PartyConnection.bandwidthFilter || queueItem.message.includes(PartyConnection.bandwidthFilter)) {
76
+ const messageSizeBits = queueItem.message.length * 8;
77
+ const transmissionTimeMs = (messageSizeBits / (PartyConnection.bandwidthKbps * 1000)) * 1000;
78
+ const bandwidthDelayMs = Math.max(transmissionTimeMs, 10);
79
+ console.log(
80
+ `\x1b[34m[BANDWIDTH SIMULATION]\x1b[0m Connection ${this.id}: Message #${queueItem.sequence} transmission time: ${bandwidthDelayMs.toFixed(1)}ms`,
81
+ );
82
+ await new Promise((resolve) => setTimeout(resolve, bandwidthDelayMs));
83
+ }
84
+ }
85
+
86
+ this.ws.send(queueItem.message);
87
+ }
88
+
89
+ this.isProcessingQueue = false;
90
+ }
91
+
92
+ private shouldApplyLatency(message: string): boolean {
93
+ if (!PartyConnection.latencyEnabled || PartyConnection.latencyMs <= 0) {
94
+ return false;
95
+ }
96
+ if (!PartyConnection.latencyFilter) {
97
+ return true;
98
+ }
99
+ return message.includes(PartyConnection.latencyFilter);
100
+ }
101
+
102
+ private async waitUntil(targetTimestamp: number): Promise<void> {
103
+ const delayMs = targetTimestamp - Date.now();
104
+ if (delayMs <= 0) {
105
+ return;
106
+ }
107
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
108
+ }
109
+
110
+ close(): void {
111
+ if (this.ws.readyState === 1) {
112
+ this.ws.close();
113
+ }
114
+ }
115
+
116
+ setState(value: any): void {
117
+ this._state = value;
118
+ }
119
+
120
+ get state(): any {
121
+ return this._state;
122
+ }
123
+
124
+ bufferIncoming(message: string, processor: (messages: string[]) => Promise<void>): void {
125
+ this.incomingQueue.push({
126
+ message,
127
+ timestamp: Date.now(),
128
+ processor,
129
+ });
130
+
131
+ if (!this.isProcessingIncomingQueue) {
132
+ void this.processIncomingQueue();
133
+ }
134
+ }
135
+
136
+ private async processIncomingQueue(): Promise<void> {
137
+ if (this.isProcessingIncomingQueue) {
138
+ return;
139
+ }
140
+ this.isProcessingIncomingQueue = true;
141
+
142
+ while (this.incomingQueue.length > 0) {
143
+ const item = this.incomingQueue.shift()!;
144
+ if (this.shouldApplyLatency(item.message)) {
145
+ await this.waitUntil(item.timestamp + PartyConnection.latencyMs);
146
+ }
147
+ try {
148
+ await item.processor([item.message]);
149
+ } catch (err) {
150
+ console.error("Error processing incoming message:", err);
151
+ }
152
+ }
153
+
154
+ this.isProcessingIncomingQueue = false;
155
+ }
156
+
157
+ static configurePacketLoss(enabled: boolean, rate: number, filter?: string): void {
158
+ PartyConnection.packetLossEnabled = enabled;
159
+ PartyConnection.packetLossRate = Math.max(0, Math.min(1, rate));
160
+ PartyConnection.packetLossFilter = filter || "";
161
+
162
+ if (enabled && rate > 0) {
163
+ const filterInfo = filter ? ` (filtered: "${filter}")` : "";
164
+ console.log(`\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Enabled with ${(rate * 100).toFixed(1)}% loss rate${filterInfo}`);
165
+ } else if (enabled) {
166
+ console.log("\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Enabled but rate is 0% (no messages will be dropped)");
167
+ } else {
168
+ console.log("\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Disabled");
169
+ }
170
+ }
171
+
172
+ static getPacketLossStatus(): { enabled: boolean; rate: number; filter: string } {
173
+ return {
174
+ enabled: PartyConnection.packetLossEnabled,
175
+ rate: PartyConnection.packetLossRate,
176
+ filter: PartyConnection.packetLossFilter,
177
+ };
178
+ }
179
+
180
+ static configureBandwidth(enabled: boolean, kbps: number, filter?: string): void {
181
+ PartyConnection.bandwidthEnabled = enabled;
182
+ PartyConnection.bandwidthKbps = Math.max(1, kbps);
183
+ PartyConnection.bandwidthFilter = filter || "";
184
+
185
+ if (enabled && kbps > 0) {
186
+ const filterInfo = filter ? ` (filtered: "${filter}")` : "";
187
+ console.log(`\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Enabled with ${kbps} kbps bandwidth${filterInfo}`);
188
+ } else if (enabled) {
189
+ console.log("\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Enabled but bandwidth is 0 kbps (no delay will be applied)");
190
+ } else {
191
+ console.log("\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Disabled");
192
+ }
193
+ }
194
+
195
+ static getBandwidthStatus(): { enabled: boolean; kbps: number; filter: string } {
196
+ return {
197
+ enabled: PartyConnection.bandwidthEnabled,
198
+ kbps: PartyConnection.bandwidthKbps,
199
+ filter: PartyConnection.bandwidthFilter,
200
+ };
201
+ }
202
+
203
+ static configureLatency(enabled: boolean, ms: number, filter?: string): void {
204
+ PartyConnection.latencyEnabled = enabled;
205
+ PartyConnection.latencyMs = Math.max(0, ms);
206
+ PartyConnection.latencyFilter = filter || "";
207
+
208
+ if (enabled && ms > 0) {
209
+ const filterInfo = filter ? ` (filtered: "${filter}")` : "";
210
+ console.log(`\x1b[35m[LATENCY SIMULATION]\x1b[0m Enabled with ${ms}ms fixed latency${filterInfo}`);
211
+ } else if (enabled) {
212
+ console.log("\x1b[35m[LATENCY SIMULATION]\x1b[0m Enabled but latency is 0ms (no delay will be applied)");
213
+ } else {
214
+ console.log("\x1b[35m[LATENCY SIMULATION]\x1b[0m Disabled");
215
+ }
216
+ }
217
+
218
+ static getLatencyStatus(): { enabled: boolean; ms: number; filter: string } {
219
+ return {
220
+ enabled: PartyConnection.latencyEnabled,
221
+ ms: PartyConnection.latencyMs,
222
+ filter: PartyConnection.latencyFilter,
223
+ };
224
+ }
225
+ }
226
+
227
+ export function logNetworkSimulationStatus(): void {
228
+ const packetLossStatus = PartyConnection.getPacketLossStatus();
229
+ const bandwidthStatus = PartyConnection.getBandwidthStatus();
230
+ const latencyStatus = PartyConnection.getLatencyStatus();
231
+
232
+ if (packetLossStatus.enabled) {
233
+ const filterInfo = packetLossStatus.filter ? ` (filter: "${packetLossStatus.filter}")` : "";
234
+ console.log(
235
+ `\x1b[36m[NETWORK SIMULATION]\x1b[0m Packet loss simulation: ${(packetLossStatus.rate * 100).toFixed(1)}% loss rate${filterInfo}`,
236
+ );
237
+ } else {
238
+ console.log("\x1b[36m[NETWORK SIMULATION]\x1b[0m Packet loss simulation: disabled");
239
+ }
240
+
241
+ if (bandwidthStatus.enabled) {
242
+ const filterInfo = bandwidthStatus.filter ? ` (filter: "${bandwidthStatus.filter}")` : "";
243
+ console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Bandwidth simulation: ${bandwidthStatus.kbps} kbps${filterInfo}`);
244
+ } else {
245
+ console.log("\x1b[36m[NETWORK SIMULATION]\x1b[0m Bandwidth simulation: disabled");
246
+ }
247
+
248
+ if (latencyStatus.enabled) {
249
+ const filterInfo = latencyStatus.filter ? ` (filter: "${latencyStatus.filter}")` : "";
250
+ console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Latency simulation: ${latencyStatus.ms}ms ping${filterInfo}`);
251
+ } else {
252
+ console.log("\x1b[36m[NETWORK SIMULATION]\x1b[0m Latency simulation: disabled");
253
+ }
254
+ }
@@ -0,0 +1,22 @@
1
+ export { PartyConnection, logNetworkSimulationStatus } from "./connection";
2
+ export {
3
+ createMapUpdateHeaders,
4
+ isMapUpdateAuthorized,
5
+ MAP_UPDATE_TOKEN_ENV,
6
+ MAP_UPDATE_TOKEN_HEADER,
7
+ readMapUpdateToken,
8
+ resolveMapUpdateToken,
9
+ } from "./map";
10
+ export { PartyRoom } from "./room";
11
+ export { createRpgServerTransport, RpgServerTransport } from "./transport";
12
+ export type {
13
+ CreateRpgServerTransportOptions,
14
+ HandleNodeRequestOptions,
15
+ RpgTransportRequestLike,
16
+ RpgTransportServer,
17
+ RpgTransportServerConstructor,
18
+ RpgWebSocketConnection,
19
+ RpgWebSocketRequestLike,
20
+ RpgWebSocketServer,
21
+ SendMapUpdateOptions,
22
+ } from "./types";