pixi-rainman-game-engine 0.3.41 → 0.3.42-beta.1

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.
@@ -7,6 +7,7 @@ import { Color } from "pixi.js";
7
7
  export const defaultAppConfig = {
8
8
  gameWidth: 1920,
9
9
  gameHeight: 1080,
10
+ enableInfinityAutoplay: false,
10
11
  reinitMode: false,
11
12
  reinitSymbolIndexFix: 0,
12
13
  idleTimeout: 10000,
@@ -13,6 +13,7 @@ export type RequiredAppConfig = {
13
13
  export type OptionalAppConfig = {
14
14
  gameWidth: number;
15
15
  gameHeight: number;
16
+ enableInfinityAutoplay: boolean;
16
17
  reinitMode: boolean;
17
18
  reinitSymbolIndexFix: number;
18
19
  idleTimeout: number;
@@ -4,6 +4,7 @@ import i18next from "i18next";
4
4
  import { observer } from "mobx-react-lite";
5
5
  import React from "react";
6
6
  import { COMPONENTS } from "../../../components";
7
+ import { RainMan } from "../../../Rainman";
7
8
  import { UI_ITEMS } from "../../../utils";
8
9
  import { useStores } from "../../hooks";
9
10
  import { CloseModalButton } from "../CloseModalButton";
@@ -11,11 +12,18 @@ import { RichLimitingSlider } from "../RichLimitingSlider";
11
12
  import { SpeedButtonWithHeader, SwitchWithHeader } from "../SwitchWithHeader";
12
13
  const initialAutoSpinCount = 10;
13
14
  const availableAutoSpinLimits = [10, 20, 50, 70, 100];
15
+ const infiniteAutoSpinCount = 1000000;
14
16
  export const AutoplaySettings = observer(() => {
15
17
  const { settingStore } = useStores();
18
+ const infinityAutoplayEnabled = RainMan.config.enableInfinityAutoplay;
16
19
  const updateLimitsBounds = (newLimit) => {
20
+ settingStore.setInfinitySpinsEnabled(false);
17
21
  setAutoSpinsCount(newLimit);
18
22
  };
23
+ const enableInfinityAutoplay = () => {
24
+ settingStore.setInfinitySpinsEnabled(true);
25
+ setAutoSpinsCount(infiniteAutoSpinCount);
26
+ };
19
27
  const { singleWinExceeds, balancedIncreased, balancedDecreased, howManyAutoSpinsLeft, infinitySpinsEnabled, setSingleWinExceeds, setBalancedIncreased, setBalancedDecreased, setAutoSpinsCount, resetAutoplaySettings } = settingStore;
20
28
  return (<div className="autoplay-settings">
21
29
  <CloseModalButton layerId={UI_ITEMS.autoplay}/>
@@ -23,9 +31,14 @@ export const AutoplaySettings = observer(() => {
23
31
  <h2>{i18next.t("autoPlay")}</h2>
24
32
  </div>
25
33
  <div className="autoplay-settings__stop-limits">
26
- {availableAutoSpinLimits.map((limiter, index) => (<button key={`${limiter}${index}`} value={limiter} className={`autoplay-settings__limit ${limiter === howManyAutoSpinsLeft ? "autoplay-settings__limit--active" : ""} middle ${infinitySpinsEnabled ? "autoplay-settings__limit--infinity" : ""}`} disabled={settingStore.infinitySpinsEnabled} onClick={(event) => updateLimitsBounds(Number(event.currentTarget.value))}>
34
+ {availableAutoSpinLimits.map((limiter, index) => (<button key={`${limiter}${index}`} value={limiter} className={`autoplay-settings__limit ${!infinitySpinsEnabled && limiter === howManyAutoSpinsLeft
35
+ ? "autoplay-settings__limit--active"
36
+ : ""} middle`} onClick={(event) => updateLimitsBounds(Number(event.currentTarget.value))}>
27
37
  <p>{limiter}</p>
28
38
  </button>))}
39
+ {infinityAutoplayEnabled && (<button value="infinity" className={`autoplay-settings__limit ${infinitySpinsEnabled ? "autoplay-settings__limit--active" : ""} right`} onClick={enableInfinityAutoplay}>
40
+ <p>∞</p>
41
+ </button>)}
29
42
  </div>
30
43
  <div className="separator"/>
31
44
  <div className="autoplay-settings__title">
@@ -16,7 +16,11 @@ export declare class SoundManagerInstance implements SpeedAdapterInterface<Sound
16
16
  readonly speedLevelMapper: SpeedLevelDirectory<SoundOverrides>;
17
17
  private musicCatalogue;
18
18
  private currentAmbientTrack;
19
+ private currentAmbientOffset;
20
+ private loadedSoundAssets;
21
+ private audioContextRecoveryPromise;
19
22
  constructor();
23
+ private loadSound;
20
24
  /**
21
25
  * Function for loading sounds into pixi
22
26
  * @public
@@ -37,7 +41,7 @@ export declare class SoundManagerInstance implements SpeedAdapterInterface<Sound
37
41
  * @returns {void}
38
42
  */
39
43
  stopPlayingFxSounds(): void;
40
- stopSoundsForPageHide(): void;
44
+ pauseSoundsForPageHide(): void;
41
45
  /**
42
46
  * Function for resuming and pausing sound in game
43
47
  * @public
@@ -61,7 +65,19 @@ export declare class SoundManagerInstance implements SpeedAdapterInterface<Sound
61
65
  getVolumeLevel(soundName: SoundTrack): number;
62
66
  private isAmbientTrack;
63
67
  private getOppositeAmbientTrack;
68
+ private getCurrentAmbientTrack;
64
69
  private getSound;
70
+ private getAmbientPlaybackOffset;
71
+ private trackAmbientPlaybackProgress;
72
+ /**
73
+ * Saves active ambient position before the browser interrupts WebAudio playback.
74
+ * @public
75
+ * @returns {void}
76
+ */
77
+ captureAmbientPlaybackPositionForPageHide(): void;
78
+ private isIOSWebAudioRuntime;
79
+ private recreatePixiSoundContext;
80
+ private recreatePixiSoundContextOnce;
65
81
  stopAmbientMusic(): void;
66
82
  /**
67
83
  * Function for resuming all sounds in game
@@ -76,6 +92,19 @@ export declare class SoundManagerInstance implements SpeedAdapterInterface<Sound
76
92
  * @returns {void}
77
93
  */
78
94
  restoreAmbientPlayback(): void;
95
+ /**
96
+ * Recreates the active ambient WebAudio source after iOS app interruptions.
97
+ * Existing Pixi instances can report isPlaying=true even when the native audio pipeline is silent.
98
+ * @private
99
+ * @returns {void}
100
+ */
101
+ private restartAmbientPlaybackAfterInterruption;
102
+ /**
103
+ * Restores ambient playback after page visibility change or iOS app interruption.
104
+ * @public
105
+ * @returns {boolean} True when audio context is ready after recovery.
106
+ */
107
+ restorePlaybackAfterPageHide(): Promise<boolean>;
79
108
  /**
80
109
  * Function for pausing all sounds in game
81
110
  * If sound is not playing, it will be skipped
@@ -121,13 +150,13 @@ export declare class SoundManagerInstance implements SpeedAdapterInterface<Sound
121
150
  /**
122
151
  * Function that resumes sounds and music when game is back in focus.
123
152
  * @public
124
- * @returns {void}
153
+ * @returns {Promise<boolean>} True when audio context is running.
125
154
  */
126
155
  resumeAudioContext(): Promise<boolean>;
127
156
  /**
128
157
  * Function that resumes sounds and music when game is back in focus.
129
158
  * @public
130
- * @returns {void}
159
+ * @returns {Promise<boolean>} True when audio context and ambient recovery are ready.
131
160
  */
132
161
  resumeOnFocus(): Promise<boolean>;
133
162
  /**
@@ -31,9 +31,25 @@ export class SoundManagerInstance {
31
31
  };
32
32
  musicCatalogue = new Map();
33
33
  currentAmbientTrack = null;
34
+ currentAmbientOffset = 0;
35
+ loadedSoundAssets = {};
36
+ audioContextRecoveryPromise = null;
34
37
  constructor() {
35
38
  this.overrides = this.speedLevelMapper.slow;
36
39
  }
40
+ loadSound(soundName, url) {
41
+ return new Promise((resolve) => {
42
+ const sound = Sound.from({
43
+ url,
44
+ autoPlay: false,
45
+ preload: true,
46
+ volume: this.getVolumeLevel(soundName),
47
+ loaded: () => resolve()
48
+ });
49
+ sound.name = soundName;
50
+ this.musicCatalogue.set(soundName, sound);
51
+ });
52
+ }
37
53
  /**
38
54
  * Function for loading sounds into pixi
39
55
  * @public
@@ -41,22 +57,13 @@ export class SoundManagerInstance {
41
57
  * @returns {Promise<void>} - promise that resolves when all sounds are loaded
42
58
  */
43
59
  async loadSounds(assets) {
60
+ this.loadedSoundAssets = { ...assets };
44
61
  const loadingSoundsPromises = [];
45
62
  for (const [soundName, url] of Object.entries(assets)) {
46
63
  if (!url) {
47
64
  continue;
48
65
  }
49
- loadingSoundsPromises.push(new Promise((resolve) => {
50
- const sound = Sound.from({
51
- url,
52
- autoPlay: false,
53
- preload: true,
54
- volume: this.getVolumeLevel(soundName),
55
- loaded: () => resolve()
56
- });
57
- sound.name = soundName;
58
- this.musicCatalogue.set(soundName, sound);
59
- }));
66
+ loadingSoundsPromises.push(this.loadSound(soundName, url));
60
67
  }
61
68
  await Promise.allSettled(loadingSoundsPromises);
62
69
  }
@@ -94,19 +101,16 @@ export class SoundManagerInstance {
94
101
  sound.stop();
95
102
  }
96
103
  }
97
- stopSoundsForPageHide() {
98
- const music = this.getSound(SoundTracks.music);
99
- const freeSpinsMusic = this.getSound(SoundTracks.freeSpinsMusic);
100
- const isMusicPlaying = Boolean(music?.isPlaying && !music.paused);
101
- const isFreeSpinsMusicPlaying = Boolean(freeSpinsMusic?.isPlaying && !freeSpinsMusic.paused);
102
- if (isFreeSpinsMusicPlaying) {
103
- this.currentAmbientTrack = SoundTracks.freeSpinsMusic;
104
- }
105
- else if (isMusicPlaying) {
106
- this.currentAmbientTrack = SoundTracks.music;
107
- }
108
- this.stopAmbientMusic();
109
- this.stopPlayingFxSounds();
104
+ pauseSoundsForPageHide() {
105
+ this.musicCatalogue.forEach((sound, soundName) => {
106
+ if (!sound.isPlaying || sound.paused) {
107
+ return;
108
+ }
109
+ if (soundName === SoundTracks.freeSpinsMusic || soundName === SoundTracks.music) {
110
+ this.currentAmbientTrack = soundName;
111
+ }
112
+ });
113
+ sound.pauseAll();
110
114
  }
111
115
  /**
112
116
  * Function for resuming and pausing sound in game
@@ -150,12 +154,15 @@ export class SoundManagerInstance {
150
154
  return;
151
155
  }
152
156
  }
153
- sound.play({
157
+ const instance = sound.play({
154
158
  singleInstance: true,
155
159
  loop: soundsToLoop.includes(soundName),
156
160
  volume: this.getVolumeLevel(soundName),
157
161
  speed: soundName === "cardDrawing" ? 0.8 : 1
158
162
  });
163
+ if (this.isAmbientTrack(soundName) && !(instance instanceof Promise)) {
164
+ this.trackAmbientPlaybackProgress(soundName, sound, instance);
165
+ }
159
166
  }
160
167
  /**
161
168
  * Returns the volume level for a given sound track.
@@ -178,10 +185,86 @@ export class SoundManagerInstance {
178
185
  getOppositeAmbientTrack(soundName) {
179
186
  return soundName === SoundTracks.freeSpinsMusic ? SoundTracks.music : SoundTracks.freeSpinsMusic;
180
187
  }
188
+ getCurrentAmbientTrack() {
189
+ return (this.currentAmbientTrack ??
190
+ (RainMan.settingsStore.isFreeSpinsStarted ? SoundTracks.freeSpinsMusic : SoundTracks.music));
191
+ }
181
192
  getSound(soundName) {
182
193
  const overrideSoundName = this.overrides[soundName];
183
194
  return this.musicCatalogue.get(overrideSoundName ?? soundName);
184
195
  }
196
+ getAmbientPlaybackOffset(sound) {
197
+ if (!sound.isPlayable || !Number.isFinite(sound.duration) || sound.duration <= 0) {
198
+ return 0;
199
+ }
200
+ return this.currentAmbientOffset % sound.duration;
201
+ }
202
+ trackAmbientPlaybackProgress(soundName, sound, instance) {
203
+ if (!sound.isPlayable || sound.duration <= 0) {
204
+ return;
205
+ }
206
+ instance.on("progress", (progress) => {
207
+ if (this.currentAmbientTrack !== soundName || !Number.isFinite(progress)) {
208
+ return;
209
+ }
210
+ this.currentAmbientOffset = (progress * sound.duration) % sound.duration;
211
+ });
212
+ }
213
+ /**
214
+ * Saves active ambient position before the browser interrupts WebAudio playback.
215
+ * @public
216
+ * @returns {void}
217
+ */
218
+ captureAmbientPlaybackPositionForPageHide() {
219
+ if (!RainMan.settingsStore.isSoundStarted || !RainMan.settingsStore.shouldPlayAmbientMusic) {
220
+ return;
221
+ }
222
+ const ambientTrack = this.getCurrentAmbientTrack();
223
+ const ambientSound = this.getSound(ambientTrack);
224
+ const ambientInstance = ambientSound?.instances.find((instance) => Number.isFinite(instance.progress) && instance.progress > 0);
225
+ if (!ambientSound || !ambientInstance || !ambientSound.isPlayable || ambientSound.duration <= 0) {
226
+ return;
227
+ }
228
+ this.currentAmbientTrack = ambientTrack;
229
+ this.currentAmbientOffset = (ambientInstance.progress * ambientSound.duration) % ambientSound.duration;
230
+ }
231
+ isIOSWebAudioRuntime() {
232
+ const userAgent = navigator.userAgent;
233
+ const platform = navigator.platform;
234
+ return /iPad|iPhone|iPod/.test(userAgent) || (platform === "MacIntel" && navigator.maxTouchPoints > 1);
235
+ }
236
+ async recreatePixiSoundContext() {
237
+ if (this.audioContextRecoveryPromise) {
238
+ return this.audioContextRecoveryPromise;
239
+ }
240
+ this.audioContextRecoveryPromise = this.recreatePixiSoundContextOnce().finally(() => {
241
+ this.audioContextRecoveryPromise = null;
242
+ });
243
+ return this.audioContextRecoveryPromise;
244
+ }
245
+ async recreatePixiSoundContextOnce() {
246
+ if (!Object.keys(this.loadedSoundAssets).length) {
247
+ return;
248
+ }
249
+ this.musicCatalogue.forEach((sound) => sound.destroy());
250
+ this.musicCatalogue.clear();
251
+ try {
252
+ sound.context.destroy();
253
+ }
254
+ catch (error) {
255
+ console.error("Could not destroy Pixi sound context", error);
256
+ }
257
+ sound.init();
258
+ sound.disableAutoPause = false;
259
+ const loadingSoundsPromises = [];
260
+ for (const [soundName, url] of Object.entries(this.loadedSoundAssets)) {
261
+ if (!url) {
262
+ continue;
263
+ }
264
+ loadingSoundsPromises.push(this.loadSound(soundName, url));
265
+ }
266
+ await Promise.allSettled(loadingSoundsPromises);
267
+ }
185
268
  stopAmbientMusic() {
186
269
  this.stop(SoundTracks.music);
187
270
  this.stop(SoundTracks.freeSpinsMusic);
@@ -196,23 +279,8 @@ export class SoundManagerInstance {
196
279
  if (!RainMan.settingsStore.isSoundStarted) {
197
280
  return;
198
281
  }
199
- this.musicCatalogue.forEach((sound, soundName) => {
200
- if (sound.paused && sound.isPlaying && sound) {
201
- sound.resume();
202
- }
203
- if (RainMan.settingsStore.shouldPlayAmbientMusic &&
204
- !RainMan.settingsStore.isFreeSpinsPlayEnabled &&
205
- soundName === SoundTracks.music &&
206
- sound.paused) {
207
- sound.resume();
208
- }
209
- if (RainMan.settingsStore.shouldPlayAmbientMusic &&
210
- RainMan.settingsStore.isFreeSpinsPlayEnabled &&
211
- soundName === SoundTracks.freeSpinsMusic &&
212
- sound.paused) {
213
- sound.resume();
214
- }
215
- });
282
+ sound.resumeAll();
283
+ this.restoreAmbientPlayback();
216
284
  }
217
285
  /**
218
286
  * Restores currently active ambient loop if browser dropped it while page was inactive.
@@ -227,10 +295,62 @@ export class SoundManagerInstance {
227
295
  this.stopAmbientMusic();
228
296
  return;
229
297
  }
230
- const ambientTrack = this.currentAmbientTrack ??
231
- (RainMan.settingsStore.isFreeSpinsStarted ? SoundTracks.freeSpinsMusic : SoundTracks.music);
298
+ const ambientTrack = this.getCurrentAmbientTrack();
232
299
  this.play(ambientTrack);
233
300
  }
301
+ /**
302
+ * Recreates the active ambient WebAudio source after iOS app interruptions.
303
+ * Existing Pixi instances can report isPlaying=true even when the native audio pipeline is silent.
304
+ * @private
305
+ * @returns {void}
306
+ */
307
+ restartAmbientPlaybackAfterInterruption() {
308
+ if (!RainMan.settingsStore.isSoundStarted) {
309
+ return;
310
+ }
311
+ if (!RainMan.settingsStore.shouldPlayAmbientMusic) {
312
+ this.stopAmbientMusic();
313
+ return;
314
+ }
315
+ const ambientTrack = this.getCurrentAmbientTrack();
316
+ const ambientSound = this.getSound(ambientTrack);
317
+ if (!ambientSound) {
318
+ return;
319
+ }
320
+ this.stop(this.getOppositeAmbientTrack(ambientTrack));
321
+ ambientSound.stop();
322
+ this.currentAmbientTrack = ambientTrack;
323
+ const instance = ambientSound.play({
324
+ singleInstance: true,
325
+ loop: soundsToLoop.includes(ambientTrack),
326
+ start: this.getAmbientPlaybackOffset(ambientSound),
327
+ volume: this.getVolumeLevel(ambientTrack),
328
+ speed: 1
329
+ });
330
+ if (!(instance instanceof Promise)) {
331
+ this.trackAmbientPlaybackProgress(ambientTrack, ambientSound, instance);
332
+ }
333
+ }
334
+ /**
335
+ * Restores ambient playback after page visibility change or iOS app interruption.
336
+ * @public
337
+ * @returns {boolean} True when audio context is ready after recovery.
338
+ */
339
+ async restorePlaybackAfterPageHide() {
340
+ if (!RainMan.settingsStore.isSoundStarted) {
341
+ return false;
342
+ }
343
+ sound.resumeAll();
344
+ if (this.isIOSWebAudioRuntime()) {
345
+ await this.recreatePixiSoundContext();
346
+ }
347
+ const isAudioContextRunning = await this.resumeAudioContext();
348
+ if (!isAudioContextRunning) {
349
+ return false;
350
+ }
351
+ this.restartAmbientPlaybackAfterInterruption();
352
+ return true;
353
+ }
234
354
  /**
235
355
  * Function for pausing all sounds in game
236
356
  * If sound is not playing, it will be skipped
@@ -238,7 +358,7 @@ export class SoundManagerInstance {
238
358
  * @returns {void}
239
359
  */
240
360
  pauseAll() {
241
- this.musicCatalogue.forEach((sound) => sound.pause());
361
+ sound.pauseAll();
242
362
  }
243
363
  /**
244
364
  * Function for stopping all sounds in game
@@ -288,7 +408,7 @@ export class SoundManagerInstance {
288
408
  /**
289
409
  * Function that resumes sounds and music when game is back in focus.
290
410
  * @public
291
- * @returns {void}
411
+ * @returns {Promise<boolean>} True when audio context is running.
292
412
  */
293
413
  async resumeAudioContext() {
294
414
  const pixiSoundContext = sound.context;
@@ -307,7 +427,7 @@ export class SoundManagerInstance {
307
427
  /**
308
428
  * Function that resumes sounds and music when game is back in focus.
309
429
  * @public
310
- * @returns {void}
430
+ * @returns {Promise<boolean>} True when audio context and ambient recovery are ready.
311
431
  */
312
432
  async resumeOnFocus() {
313
433
  if (!RainMan.settingsStore.isSoundStarted) {
@@ -317,8 +437,7 @@ export class SoundManagerInstance {
317
437
  if (!isAudioContextRunning) {
318
438
  return false;
319
439
  }
320
- this.restoreAmbientPlayback();
321
- return true;
440
+ return this.restorePlaybackAfterPageHide();
322
441
  }
323
442
  /**
324
443
  * Returns information if audio context is ready to play sounds.
@@ -563,6 +563,10 @@ export class AbstractMainContainer extends Container {
563
563
  this.messageBox.visible = true;
564
564
  RainMan.settingsStore.isGambleGameActive = false;
565
565
  this._controller.updateGambleValues(RainMan.settingsStore.gambleGameWin);
566
+ if (RainMan.settingsStore.gambleGameWin > 0) {
567
+ RainMan.settingsStore.useTakeAction?.();
568
+ }
569
+ RainMan.settingsStore.disableSpecialButtons();
566
570
  this.gambleGame = null;
567
571
  }
568
572
  this.hideOverlay();
@@ -88,6 +88,8 @@ export class GambleGame extends Container {
88
88
  this.redButton.setOnClick(() => this.handleGamblePlay("red"));
89
89
  this.blackButton.setOnClick(() => this.handleGamblePlay("black"));
90
90
  this.collectButton.setOnClick(() => {
91
+ SoundManager.stop("cardDrawing");
92
+ SoundManager.play("music");
91
93
  RainMan.settingsStore.gambleGameWin = this.lastWinAmount;
92
94
  RainMan.componentRegistry.get(BUTTONS.gamble).flipDisabledFlag(true);
93
95
  SoundManager.play("cardCollect");
@@ -210,8 +212,10 @@ export class GambleGame extends Container {
210
212
  this.card.state.addAnimation(0, "idle", true, 0);
211
213
  await this.loseDoubleAndWinFields();
212
214
  window.setTimeout(() => {
215
+ SoundManager.stop("cardDrawing");
216
+ SoundManager.play("music");
213
217
  this.closeGame();
214
- }, 2000);
218
+ }, 500);
215
219
  }
216
220
  }
217
221
  /**
@@ -34,6 +34,8 @@ export declare class MessageBox extends Container {
34
34
  private possibleWinShown;
35
35
  private incentiveMessageShown;
36
36
  private winLineIndicatorShown;
37
+ private winLineIndicatorXOffset;
38
+ private winLineIndicatorYOffset;
37
39
  constructor();
38
40
  /**
39
41
  * Updates the incentive text displayed in the message box.
@@ -84,6 +86,15 @@ export declare class MessageBox extends Container {
84
86
  * @returns {void}
85
87
  */
86
88
  hideWinLineIndicator(): void;
89
+ /**
90
+ * Sets the offset of the win line indicator along the x and y axes. Each time winLineIndicator is shown,
91
+ * the indicator is positioned at the current offset.
92
+ * @public
93
+ * @param {number} xOffset - The amount to move the win line indicator along the x-axis.
94
+ * @param {number} yOffset - The amount to move the win line indicator along the y-axis.
95
+ * @returns {void}
96
+ */
97
+ setWinWinLineIndicatorOffset(xOffset?: number, yOffset?: number): void;
87
98
  /**
88
99
  * Function for resizing message box
89
100
  * It sets the scale of the message box to the global minimum ratio.
@@ -39,6 +39,8 @@ export class MessageBox extends Container {
39
39
  possibleWinShown = false;
40
40
  incentiveMessageShown = false;
41
41
  winLineIndicatorShown = false;
42
+ winLineIndicatorXOffset = 0;
43
+ winLineIndicatorYOffset = 0;
42
44
  constructor() {
43
45
  super();
44
46
  this.name = MessageBox.registryName;
@@ -165,6 +167,18 @@ export class MessageBox extends Container {
165
167
  this.winLineIndicator = undefined;
166
168
  }
167
169
  }
170
+ /**
171
+ * Sets the offset of the win line indicator along the x and y axes. Each time winLineIndicator is shown,
172
+ * the indicator is positioned at the current offset.
173
+ * @public
174
+ * @param {number} xOffset - The amount to move the win line indicator along the x-axis.
175
+ * @param {number} yOffset - The amount to move the win line indicator along the y-axis.
176
+ * @returns {void}
177
+ */
178
+ setWinWinLineIndicatorOffset(xOffset, yOffset) {
179
+ this.winLineIndicatorXOffset += xOffset ?? 0;
180
+ this.winLineIndicatorYOffset += yOffset ?? 0;
181
+ }
168
182
  /**
169
183
  * Function for resizing message box
170
184
  * It sets the scale of the message box to the global minimum ratio.
@@ -334,6 +348,12 @@ export class MessageBox extends Container {
334
348
  };
335
349
  this.winLineIndicator = new WinLineIndicator(winLineTextStyle, getImageForSymbolId, message, symbolIds);
336
350
  this.winLineIndicator.y = 50;
351
+ if (this.winLineIndicatorXOffset) {
352
+ this.winLineIndicator.x = this.winLineIndicatorXOffset;
353
+ }
354
+ if (this.winLineIndicatorYOffset) {
355
+ this.winLineIndicator.y = this.winLineIndicatorYOffset;
356
+ }
337
357
  this.winLineIndicatorShown = true;
338
358
  this.centerTextOnX(this.winLineIndicator);
339
359
  this.addChild(this.winLineIndicator);
@@ -74,6 +74,7 @@ export interface MiniGameInterface {
74
74
  };
75
75
  extra_data: ExtraData;
76
76
  win_amount: number;
77
+ round_win_amount: number;
77
78
  wins: Record<string, RawWin>;
78
79
  matrix: {
79
80
  height: number;
@@ -311,6 +311,7 @@ export class AbstractController {
311
311
  if (RainMan.componentRegistry.has(BUTTONS.gamble)) {
312
312
  this.buttonsEventManager.initGambleButton(() => {
313
313
  RainMan.settingsStore.isGambleGameAvailable = false;
314
+ SoundManager.stopAll();
314
315
  this.mainContainer.invokeGambleGame();
315
316
  }, !RainMan.settingsStore.isGambleGameAvailable ||
316
317
  RainMan.settingsStore.isPlayingAnyAction ||
@@ -841,6 +842,13 @@ export class AbstractController {
841
842
  this.uiController.totalNumberOfFreeSpins += this.getBoughtNumberOfFreeSpins();
842
843
  RainMan.componentRegistry.getIfExists(FreeSpinButton.registryName)?.setInteractivity(false);
843
844
  RainMan.settingsStore.setIsFreeSpinBought(false);
845
+ if (this.speedLevelBeforeFreeSpins === null) {
846
+ this.speedLevelBeforeFreeSpins = RainMan.settingsStore.currentSpeed;
847
+ }
848
+ RainMan.settingsStore.setIsTemporarySpeed(false);
849
+ RainMan.componentRegistry.setSpeedLevel(SPEED_LEVELS.slow);
850
+ this.uiController.updateShownSpeedLevel(SPEED_LEVELS.slow);
851
+ this.disableSpeedButton(false);
844
852
  }
845
853
  /**
846
854
  * This function is responsible for getting the number of free spins bought
@@ -903,6 +911,7 @@ export class AbstractController {
903
911
  logInfo("Take action can't be performed");
904
912
  return;
905
913
  }
914
+ this.shouldPlayNextSpin = false;
906
915
  if (this.currentlyPlayedAction !== undefined) {
907
916
  this.changeGamePhase(gamePhases.STOPPING_HANDLING_ACTIONS);
908
917
  this.stopPlayingSymbolsStreak();
@@ -913,7 +922,7 @@ export class AbstractController {
913
922
  if (takeData.status.code === 200) {
914
923
  RainMan.settingsStore.isTakeActionAvailable = false;
915
924
  logInfo(`💰 Round ${takeData.round_number} ended with balance:`, takeData.balance);
916
- this.uiController.currentBalance = takeData.balance;
925
+ await this.uiController.animateCurrentBalance(takeData.balance);
917
926
  }
918
927
  else {
919
928
  RainMan.settingsStore.isTakeActionAvailable = true;
@@ -1219,7 +1228,9 @@ export class AbstractController {
1219
1228
  }
1220
1229
  await this.performActionsAfterSpin();
1221
1230
  this.freeSpinsWin = gambleWinAmount > 0 ? gambleWinAmount : null;
1222
- this.gambleGameEnabler(gambleWinAmount);
1231
+ if (!RainMan.globals.isSuperBonusGameEnabled) {
1232
+ this.gambleGameEnabler(gambleWinAmount);
1233
+ }
1223
1234
  if (RainMan.settingsStore.isAutoplayEnabled ||
1224
1235
  (RainMan.settingsStore.howManyFreeSpinsLeft !== 0 && this.shouldAutoplayFreeSpins)) {
1225
1236
  this.handleAutoplayLogic();
@@ -149,7 +149,6 @@ export declare class UiController {
149
149
  updateShownPossibleWin(): void;
150
150
  /**
151
151
  * Shows game pays in a bottom panel, if exists
152
- * @param amount
153
152
  * @public
154
153
  * @returns {void}
155
154
  */
@@ -189,6 +188,13 @@ export declare class UiController {
189
188
  * @returns {Promise<void>}
190
189
  */
191
190
  updateDisplayedBalance(amount: number): Promise<void>;
191
+ /**
192
+ * Function for animating current balance to a final value.
193
+ * @public
194
+ * @param {number} finalBalance - final balance to display
195
+ * @returns {Promise<void>}
196
+ */
197
+ animateCurrentBalance(finalBalance: number): Promise<void>;
192
198
  /**
193
199
  * Function for resetting win amount
194
200
  * @public
@@ -216,7 +216,6 @@ export class UiController {
216
216
  }
217
217
  /**
218
218
  * Shows game pays in a bottom panel, if exists
219
- * @param amount
220
219
  * @public
221
220
  * @returns {void}
222
221
  */
@@ -282,6 +281,23 @@ export class UiController {
282
281
  async updateDisplayedBalance(amount) {
283
282
  this.promisesToSetBalances.push(RainMan.componentRegistry.get(COMPONENTS.creditText).update(amount));
284
283
  }
284
+ /**
285
+ * Function for animating current balance to a final value.
286
+ * @public
287
+ * @param {number} finalBalance - final balance to display
288
+ * @returns {Promise<void>}
289
+ */
290
+ async animateCurrentBalance(finalBalance) {
291
+ const balanceDiff = ceilToDecimal(finalBalance - this._currentBalance);
292
+ this._currentBalance = finalBalance;
293
+ if (balanceDiff === 0) {
294
+ this.setDisplayedBalance();
295
+ return;
296
+ }
297
+ const animationPromise = RainMan.componentRegistry.get(COMPONENTS.creditText).update(balanceDiff);
298
+ this.promisesToSetBalances.push(animationPromise);
299
+ await animationPromise;
300
+ }
285
301
  /**
286
302
  * Function for resetting win amount
287
303
  * @public
@@ -24,7 +24,7 @@ export class AbstractLoadingContainer extends Container {
24
24
  this.name = "loadingContainer";
25
25
  this.x = 0;
26
26
  this.y = 0;
27
- this.loaderContainer = new SpriteLoadingContainer("./assets/images/materials/texture.png");
27
+ this.loaderContainer = new SpriteLoadingContainer("./assets/images/materials/texture.webp");
28
28
  this.presentationFinishedPromise = new Promise((resolve) => {
29
29
  this.resolvePresentationFinished = resolve;
30
30
  });
@@ -113,6 +113,12 @@ export declare class SettingsStore {
113
113
  * @returns {void}
114
114
  */
115
115
  disableSpecialButtons(): void;
116
+ /**
117
+ * Enables special buttons like gamble and take.
118
+ * @public
119
+ * @returns {void}
120
+ */
121
+ enableSpecialButtons(): void;
116
122
  /**
117
123
  * Swaps the bet change disabled flag.
118
124
  * @public
@@ -338,6 +344,13 @@ export declare class SettingsStore {
338
344
  * @returns {void}
339
345
  */
340
346
  setAutoSpinsCount(count: number): void;
347
+ /**
348
+ * Setter for the infinite autoplay flag
349
+ * @public
350
+ * @param {boolean} newState new state for infinite autoplay
351
+ * @returns {void}
352
+ */
353
+ setInfinitySpinsEnabled(newState: boolean): void;
341
354
  /**
342
355
  * Setter for the autoplay enabled flag
343
356
  * @public
@@ -79,6 +79,7 @@ export class SettingsStore {
79
79
  initialAutoplaySettingsState = {
80
80
  onAnyWin: false,
81
81
  howManyAutoSpinsLeft: INITIAL_AUTO_SPIN_COUNT,
82
+ infinitySpinsEnabled: false,
82
83
  singleWinExceeds: 0,
83
84
  balancedIncreased: 0,
84
85
  balancedDecreased: 0,
@@ -185,6 +186,21 @@ export class SettingsStore {
185
186
  RainMan.componentRegistry.get(BUTTONS.take).flipDisabledFlag(true);
186
187
  }
187
188
  }
189
+ /**
190
+ * Enables special buttons like gamble and take.
191
+ * @public
192
+ * @returns {void}
193
+ */
194
+ enableSpecialButtons() {
195
+ if (RainMan.componentRegistry.has(BUTTONS.gamble)) {
196
+ RainMan.settingsStore.isGambleGameAvailable = true;
197
+ RainMan.componentRegistry.get(BUTTONS.gamble).flipDisabledFlag(false);
198
+ }
199
+ if (RainMan.componentRegistry.has(BUTTONS.take)) {
200
+ RainMan.settingsStore.isTakeActionAvailable = true;
201
+ RainMan.componentRegistry.get(BUTTONS.take).flipDisabledFlag(false);
202
+ }
203
+ }
188
204
  /**
189
205
  * Swaps the bet change disabled flag.
190
206
  * @public
@@ -576,6 +592,15 @@ export class SettingsStore {
576
592
  setAutoSpinsCount(count) {
577
593
  this.howManyAutoSpinsLeft = count;
578
594
  }
595
+ /**
596
+ * Setter for the infinite autoplay flag
597
+ * @public
598
+ * @param {boolean} newState new state for infinite autoplay
599
+ * @returns {void}
600
+ */
601
+ setInfinitySpinsEnabled(newState) {
602
+ this.infinitySpinsEnabled = newState;
603
+ }
579
604
  /**
580
605
  * Setter for the autoplay enabled flag
581
606
  * @public
@@ -713,6 +738,7 @@ export class SettingsStore {
713
738
  resetAutoplaySettings() {
714
739
  this.onAnyWin = this.initialAutoplaySettingsState.onAnyWin;
715
740
  this.howManyAutoSpinsLeft = this.initialAutoplaySettingsState.howManyAutoSpinsLeft;
741
+ this.infinitySpinsEnabled = this.initialAutoplaySettingsState.infinitySpinsEnabled;
716
742
  this.singleWinExceeds = this.initialAutoplaySettingsState.singleWinExceeds;
717
743
  this.balancedIncreased = this.initialAutoplaySettingsState.balancedIncreased;
718
744
  this.balancedDecreased = this.initialAutoplaySettingsState.balancedDecreased;
@@ -16,15 +16,16 @@ const interruptedAudioResume = () => {
16
16
  window.addEventListener("touchend", resumeAudioOnInterruption, { once: true, passive: true });
17
17
  };
18
18
  /**
19
- * Pauses audio when the page goes to the background.
19
+ * Clears pending foreground recovery when the page goes to the background.
20
+ * Pixi handles the actual pause state through its WebAudio auto-pause flow.
20
21
  * @returns {void}
21
22
  */
22
- const pauseAudio = () => {
23
+ const prepareAudioForPageHide = () => {
23
24
  isResumingAudio = false;
24
25
  shouldResumeAudioOnInterruption = false;
25
26
  removeInterruptedAudioResumeListeners();
26
27
  if (RainMan.settingsStore.isSoundEnabled()) {
27
- SoundManager.stopSoundsForPageHide();
28
+ SoundManager.captureAmbientPlaybackPositionForPageHide();
28
29
  }
29
30
  };
30
31
  /**
@@ -71,19 +72,19 @@ function resumeAudioOnInterruption() {
71
72
  */
72
73
  const handleVisibilityChange = () => {
73
74
  if (document.hidden) {
74
- pauseAudio();
75
+ prepareAudioForPageHide();
75
76
  return;
76
77
  }
77
- interruptedAudioResume();
78
+ resumeAudio();
78
79
  };
79
80
  /**
80
81
  * Sets up audio states for tab switches and iOS app interruptions.
81
82
  * @returns {void}
82
83
  */
83
84
  export const loadSoundsOnTabChange = () => {
84
- sound.disableAutoPause = true;
85
+ sound.disableAutoPause = false;
85
86
  document.addEventListener("visibilitychange", handleVisibilityChange);
86
- window.addEventListener("pagehide", () => pauseAudio());
87
- window.addEventListener("pageshow", () => interruptedAudioResume());
88
- window.addEventListener("focus", () => interruptedAudioResume());
87
+ window.addEventListener("pagehide", () => prepareAudioForPageHide());
88
+ window.addEventListener("pageshow", () => resumeAudio());
89
+ window.addEventListener("focus", () => resumeAudio());
89
90
  };
@@ -25,6 +25,8 @@ export declare class TexturedText extends UpdatableValueComponent {
25
25
  * @returns {void}
26
26
  */
27
27
  randomizeSet(value: number, _possibleValues: number[]): void;
28
+ adjustTextureSpriteLength(): void;
29
+ set(newValue: number): void;
28
30
  /**
29
31
  * Plays the animation for the text element
30
32
  * @public
@@ -18,6 +18,7 @@ export class TexturedText extends UpdatableValueComponent {
18
18
  this.registryName = registryName;
19
19
  this.valueTextComponent.style = { ...this.valueTextComponent.style, fontSize: 120, padding: 50 };
20
20
  this.valueTextComponent.anchor.set(0.5, 0.5);
21
+ this.updateFontSize();
21
22
  if (textureName) {
22
23
  this.textureSprite = new TilingSprite(getTexture(textureName), this.valueTextComponent.width, this.valueTextComponent.height);
23
24
  this.textureSprite.scale.set(1.1);
@@ -48,6 +49,17 @@ export class TexturedText extends UpdatableValueComponent {
48
49
  this.valueTextComponent.style.fontSize = -10 * new MoneyText(value).toString().length + this.baseFontSize;
49
50
  super.update(value, 1);
50
51
  }
52
+ adjustTextureSpriteLength() {
53
+ if (!this.textureSprite) {
54
+ return;
55
+ }
56
+ this.textureSprite.width = this.valueTextComponent.width;
57
+ this.textureSprite.height = this.valueTextComponent.height;
58
+ }
59
+ set(newValue) {
60
+ super.set(newValue);
61
+ this.adjustTextureSpriteLength();
62
+ }
51
63
  /**
52
64
  * Plays the animation for the text element
53
65
  * @public
@@ -89,5 +101,6 @@ export class TexturedText extends UpdatableValueComponent {
89
101
  */
90
102
  updateFontSize() {
91
103
  this.valueTextComponent.style.fontSize = -10 * super.value.length + this.baseFontSize;
104
+ this.adjustTextureSpriteLength();
92
105
  }
93
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-rainman-game-engine",
3
- "version": "0.3.41",
3
+ "version": "0.3.42-beta.1",
4
4
  "description": "This repository contains all of the mechanics that used in rainman games.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",