This commit is contained in:
Andrey Sharshov
2025-11-16 18:54:31 +01:00
commit 9487728656
2342 changed files with 62687 additions and 0 deletions

62
utils/GameErrors.js Normal file
View File

@@ -0,0 +1,62 @@
import {Locator} from "@popiplay/slot-game-kit";
function getErrorDescriptionByCode(code) {
switch (code) {
case 101:
case 102:
case 103:
case 104:
case 105:
case 201:
case 202:
case 203:
case 204:
case 205:
return Locator.locales.get("the_bet_has_not_been_accepted");
case 206:
return Locator.locales.get("your_freespins_are_ended");
case 301:
return Locator.locales.get('you_have_insufficient_funds');
case 302:
case 303:
case 401:
case 402:
case "301nodep":
case "206i":
case "some_error":
case "many_tabs_error":
return Locator.locales.get(code);
case "default":
default: {
const textLocale = Locator.locales.get("default").split("{code}");
return `${textLocale[0]} ${code} ${textLocale[1]}`;
}
}
}
export function checkForErrors(response, showPreloader) {
switch (true) {
case (response instanceof NetworkError):
throw response;
case ("errors" in response):
throw new ResponseResultError(response.errors[0].code, showPreloader);
}
}
export class ResponseResultError extends Error {
/**
*
* @param {number} code the response error code
*/
constructor(code, showPreloader) {
super(getErrorDescriptionByCode(code));
this.code = code;
this.showPreloader = showPreloader;
}
}
export class NetworkError extends Error {
constructor() {
super("The client can't connect to the server");
}
}

20
utils/Numbers.js Normal file
View File

@@ -0,0 +1,20 @@
export default class Numbers {
static format(number, exponent = 2) {
if (typeof number !== 'number') return number;
if (number === 0) {
return '0';
}
return number.toFixed(exponent)
}
static getInfinityInsteadOfNegative(number) {
if (number < 0) {
return '∞';
}
return number;
}
static addThousandSeparators(number, delimiter = ',') {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, delimiter);
}
}

View File

@@ -0,0 +1,28 @@
import { Container, Transform } from 'pixi.js';
import { div } from './htmlUtils.js';
import {Locator} from "@popiplay/slot-game-kit";
export class PixiHtmlContainer extends Container {
constructor(parentDom, className, style = {}) {
super();
this.element = div(className);
this.domTransform = new Transform();
Object.assign(this.element.style, style);
parentDom?.append(this.element);
Locator.viewport.on('resize', this.updateTransform, this);
}
updateTransform() {
super.updateTransform();
const globalTransform = this.parent.transform.worldTransform;
const decomposition = globalTransform.decompose(this.domTransform);
let transform = `scale(${decomposition.scale.x}, ${decomposition.scale.y}) translate(-50%, -50%)`
this.element.style.transform = transform;
}
destroy(...args) {
Locator.viewport.off('resize', this.updateTransform, this);
super.destroy(...args);
}
}

16
utils/SeededRandom.js Normal file
View File

@@ -0,0 +1,16 @@
export class SeededRandom {
constructor(seed) {
this.seed = seed;
}
// Generates a random number
next() {
this.seed = (this.seed * 9301 + 49297) % 233280;
return this.seed / 233280;
}
// Generates a random integer within a range
nextInt(min, max) {
return Math.floor(this.next() * (max - min + 1)) + min;
}
}

8
utils/SpineHelper.js Normal file
View File

@@ -0,0 +1,8 @@
export class SpineHelper {
static skip(spine, trackIndex = 0) {
const track = spine.state.tracks[trackIndex];
track.trackTime = track.animation.duration;
track.loop = false;
spine.update(0);
}
}

30
utils/TextHelper.js Normal file
View File

@@ -0,0 +1,30 @@
export class TextHelper {
static scaleToFit(textObject, size) {
if (!size.width) size.width = Infinity;
if (!size.height) size.height = Infinity;
if (textObject.style.wordWrap) {
wordWrapScaleToFit(textObject, size);
} else {
nonWordWrapScaleToFit(textObject, size);
}
}
}
function wordWrapScaleToFit(textObject, size) {
textObject.scale.set(1);
if (!textObject.style.defaultWordWrapWidth) {
textObject.style.defaultWordWrapWidth = textObject.style.wordWrapWidth;
}
textObject.style.wordWrapWidth = textObject.style.defaultWordWrapWidth;
const scaleStep = 0.99;
while (size.width < textObject.width || size.height < textObject.height) {
textObject.scale.set(textObject.scale.x * scaleStep, textObject.scale.y * scaleStep);
textObject.style.wordWrapWidth = textObject.style.wordWrapWidth / scaleStep;
}
}
function nonWordWrapScaleToFit(textObject, size) {
textObject.scale.set(1);
const factor = Math.min(1, size.width / textObject.width, size.height / textObject.height);
textObject.scale.set(factor);
}

88
utils/create.js Normal file
View File

@@ -0,0 +1,88 @@
import * as PIXI from "pixi.js";
import {Assets} from "pixi.js";
export const createElement = (displayObject, props, ...children) => {
if (!props) props = {};
if (!props.args) props.args = [];
const obj = new displayObject(...props.args)
if (props && props.ref) props.ref(obj);
children.forEach(child => addChild(obj, child));
obj.once('added', () => {
if (props) setProps(obj, props);
})
return obj
}
export const addChild = (parent, children) => {
if (Array.isArray(children))
children.forEach(nestedChild => {
addChild(parent, nestedChild)
});
else {
parent.addChild(children)
}
};
export function setProps(object, props) {
Object.keys(props).forEach(property => {
if (property === 'texture') {
object.texture = getTexture(props.texture);
return;
}
if (property === 'textures') {
object.textures = getTextures(props.textures)
return;
}
if (property === 'styles') {
applyStyles(object, props)
return;
}
if (property === 'position') {
object.position.set(...props.position)
return;
}
object[property] = props[property]
})
}
export function getTexture(texture) {
if (typeof texture === 'string') {
return Assets.get(texture)
}
if (texture instanceof PIXI.Texture) {
return texture;
}
return PIXI.Texture.WHITE
}
function getTextures(textures) {
return textures.map((texture) => getTexture(texture))
}
export function applyStyles(object, props) {
if (!object.styles) {
Object.defineProperty(object, 'styles', {
get: () => object._styles,
set: (value) => {
if (!object._styles) object._styles = {};
Object.assign(object._styles, value);
const styles = {}
for (const key in object._styles) {
if (typeof object._styles[key] === 'function') {
styles[key] = object._styles[key]();
} else {
styles[key] = object._styles[key];
}
}
setProps(object, styles)
}
})
}
object.styles = props.styles
}

20
utils/delayedPromise.js Normal file
View File

@@ -0,0 +1,20 @@
/**
*
* @returns {(Promise & {resolve: (r?: any)=>void; reject: (reason)=>void;})}
*/
export function delayedPromise() {
let res = null;
let rej = null;
const deferredPromise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
Object.assign(deferredPromise, {
resolve: res,
reject: rej,
});
return deferredPromise;
}

View File

@@ -0,0 +1,11 @@
export default function handleShowRTPInRulesOption() {
if (!__OPTIONS__.ui.show_rtp_in_rules) {
const styleEl = document.createElement('style');
styleEl.textContent = `
#section_rtp {
display: none !important;
}
`;
document.head.appendChild(styleEl);
}
}

12
utils/htmlUtils.js Normal file
View File

@@ -0,0 +1,12 @@
export function div(className) {
const view = document.createElement('div');
view.className = className;
return view;
}
export function button(name, callback, className = '') {
const view = document.createElement('button');
view.className = className;
view.addEventListener('pointerdown', callback);
view.innerHTML = name;
return view;
}

7
utils/isIframe.js Normal file
View File

@@ -0,0 +1,7 @@
export function isInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}

11
utils/isSafariOnIOS.js Normal file
View File

@@ -0,0 +1,11 @@
export function isSafariOnIphone() {
var userAgent = window.navigator.userAgent;
var isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream;
var isChrome = /CriOS/.test(userAgent);
var isOpera = /OPiOS/.test(userAgent);
var isSafari = /Safari/.test(userAgent) && !isChrome && !isOpera;
if (isIOS && isChrome && !isOpera) {
return false;
} else return isIOS && isSafari;
}

22
utils/parseWinsData.js Normal file
View File

@@ -0,0 +1,22 @@
export const parseWinsData = (winsArray, currency) => {
if (!winsArray || winsArray.length === 0) return ;
const totalWin = winsArray.reduce((total, [_, win]) => {
return total + win;
}, 0);
const totalWinString = currency.getFormattedValue(totalWin);
const wins = winsArray.map(([type, amount, map, lineIndex]) => {
return {
type,
amount,
amountString: currency.getFormattedValue(amount),
lineSlotsIndexMap: map,
lineIndex
}
})
const result = {
totalWin,
totalWinString,
wins
}
return result
}

21
utils/sentry.js Normal file
View File

@@ -0,0 +1,21 @@
import * as Sentry from '@sentry/browser';
const isSentryAvailable = !!__SENTRY_DSN && !!__SENTRY_RELEASE;
export function initSentry() {
if(!isSentryAvailable) return undefined;
const sentry = Sentry.init({
environment: window.__OPTIONS__.environment,
dsn: __SENTRY_DSN,
release: __SENTRY_RELEASE,
attachStacktrace: true,
ignoreErrors: [],
});
console.log({ __SENTRY_RELEASE, __SENTRY_DSN });
if (window.user_id) Sentry.setUser({id: window.user_id});
return sentry;
}

54
utils/setupDom.js Normal file
View File

@@ -0,0 +1,54 @@
/**
* Sets up the DOM structure for the canvas.
*
* @param {HTMLElement} canvas - The canvas element to be inserted into the DOM.
*/
export default function setupDom(canvas, groot) {
document.body.addEventListener('contextmenu', (e) => e.preventDefault());
const content = document.createElement('div');
content.classList.add('content');
document.body.appendChild(content);
const container = document.createElement('div');
container.classList.add('canvas-wrapper');
content.appendChild(container);
canvas.classList.add('webgl');
container.appendChild(canvas);
groot && groot.appendChild(container);
groot && content.appendChild(groot);
const spacer = document.createElement('div');
spacer.classList.add('spacer');
document.body.appendChild(spacer);
// trick FairyGUI Stage styles
enableTouchScroll();
}
export function enableTouchScroll(selector = 'body') {
const el = document.querySelector(selector);
if (el) {
el.style.setProperty('touch-action', 'auto', 'important');
}
// Delete global * { touch-action: none }
[...document.styleSheets].forEach((sheet) => {
try {
[...sheet.cssRules].forEach((rule, index) => {
if (rule.selectorText === '*' && rule.cssText.includes('touch-action')) {
sheet.deleteRule(index);
}
});
} catch (e) {
// Ignore errors if CORS
}
});
}
export function disableTouchScroll(selector = 'body') {
const el = document.querySelector(selector);
if (el) {
el.style.setProperty('touch-action', 'none', 'important');
}
}

19
utils/styles.js Normal file
View File

@@ -0,0 +1,19 @@
export const createStyles = (sheet) => {
return new Proxy(sheet, {
get: (target, property, receiver) => {
const medias = getMedia(target).filter((media) => {
if (!target[media].hasOwnProperty(property)) return false;
return matchMedia(media).matches;
});
if (medias.length === 0) return target[property];
return target[medias[medias.length-1]][property];
}
})
}
function getMedia(target) {
const properties = Object.keys(target);
const regex = new RegExp(/\(.*\)/);
return properties.filter((property) => regex.test(property))
}

151
utils/utils.js Normal file
View File

@@ -0,0 +1,151 @@
import { Locator } from "@popiplay/slot-game-kit";
import { SKIN_TYPES } from "../src/config";
/**
* Creates a callback function that executes a specific callback after a defined count of invocations.
*
* @param {number} n - The count threshold that triggers the callback execution.
* @param {Function} callback - The function to execute after `n` invocations. Defaults to an empty function.
* @param {Function} eachCallback - The function to execute on each invocation. Defaults to an empty function.
* @return {Function} A new function that increments a count each time it's called, executes `eachCallback`,
* and if the count equals `n`, executes `callback` and resets the count.
*
* @example
* // Logs 'Hello, third time!' every third invocation
* const logEveryTime = (...args) => console.log('Called with', args);
* const sayHelloEveryThirdTime = () => console.log('Hello, third time!');
* const thresholdCallback = createThresholdCallback(3, sayHelloEveryThirdTime, logEveryTime);
*
* thresholdCallback('test'); // Logs 'Called with ["test"]'
* thresholdCallback('again'); // Logs 'Called with ["again"]'
* thresholdCallback('and again'); // Logs 'Called with ["and again"]' and 'Hello, third time!'
* thresholdCallback('another one'); // Logs 'Called with ["another one"]'
*
*/
export function createThresholdCallback(
n,
callback = () => { },
eachCallback = () => { }
) {
let counter = 0;
return function (...args) {
counter++;
eachCallback(...args);
if (n === counter) {
counter = 0;
callback(...args);
}
}
}
/**
* Asynchronously waits for a specified event to be emitted once on the target object.
*
* @async
* @param {EventEmitter} target - The EventEmitter object on which to listen for the event.
* @param {string} event - The name of the event to listen for.
* @return {Promise<void>} A promise that resolves when the specified event is emitted on the target.
*
* Usage:
* ```javascript
* await waitForEventOnce(someObject, 'eventName');
* // Following code won't execute until 'eventName' is emitted on 'someObject' for the first time.
* ```
*/
export async function waitForEventOnce(target, event) {
return new Promise((resolve) => {
target.once(event, () => {
resolve()
});
})
}
/**
* Asynchronously waits for the first event from a list of events to be emitted.
*
* @async
* @param {Array<{ target: EventEmitter, event: string }>} events - An array of objects, each containing an EventEmitter object and the event to listen for.
* @return {Promise<void>} A Promise that resolves as soon as one of the specified events is emitted on its respective EventEmitter.
*
* Usage:
* ```javascript
* const events = [
* { target: someObject1, event: 'eventName1'},
* { target: someObject2, event: 'eventName2'}
* ];
*
* await waitForFirstEmittedEvent(events);
* // Following code won't execute until either 'eventName1' is emitted on 'someObject1' or 'eventName2' is emitted on 'someObject2' for the first time.
* ```
*/
export async function waitForFirstEmittedEvent(events) {
const promises = events.map(({ target, event }) => {
return waitForEventOnce(target, event);
})
return Promise.race(promises)
}
export function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
export const transpose = (matrix) => matrix[0].map((_, i) => matrix.map(row => row[i]));
export const isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj);
/**
* @description - Deeply merges two objects.
*
* @param {Object} target - The target object to merge properties into.
* @param {Object} source - The source object from which to copy properties.
* @return {Object} The merged object.
*
* @example
* const obj1 = { a: 1, b: { c: 2 } };
* const obj2 = { b: { d: 3 }, e: 4 };
* const result = deepMerge(obj1, obj2);
* // result is { a: 1, b: { c: 2, d: 3 }, e: 4 }
*/
export function deepMerge(target, source) {
if (!isObject(target) || !isObject(source)) {
return source;
}
const merged = { ...target };
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!target[key]) {
merged[key] = source[key];
} else {
merged[key] = deepMerge(target[key], source[key]);
}
} else {
merged[key] = source[key];
}
});
return merged;
}
export const getSkinType = (threshhold) => {
return Locator.viewport.cropWidthRatio < threshhold
? SKIN_TYPES.PORT
: SKIN_TYPES.LAND;
}
export const getStyle = (styleTempl, dynamicStyle, threshhold) => {
const skinType = getSkinType(threshhold);
const hasDynamicStyle = !!dynamicStyle
&& typeof dynamicStyle === 'object'
&& !!Object.entries(dynamicStyle).length;
const additionalStyles = dynamicStyle?.[skinType] || dynamicStyle;
return hasDynamicStyle ? deepMerge(styleTempl, additionalStyles) : styleTempl;
}
export const getRandomInt = (min, max) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}