This commit is contained in:
Nordi98 2025-08-11 16:51:34 +02:00
parent 600d79af31
commit 5d11084641
136 changed files with 12007 additions and 584 deletions

View file

@ -0,0 +1,224 @@
<script lang="ts">
// Svelte
import { fade } from 'svelte/transition';

// Stores
import { isDevMode, showComponent } from './stores/GeneralStores';
import { showUi } from './stores/GeneralStores';

// Enums
import { UIComponentsEnum } from './enums/UIComponentsEnum';

// PS-UI components
import GameLauncher from './games/GameLauncher.svelte';
import { EventHandler, handleKeyUp } from './../utils/eventHandler';
import Image from './components/Image.svelte';
import { notificationMock } from './../utils/mockEvent';
import Notification from './components/Notification.svelte';
import { GamesEnum } from './enums/GamesEnum';
import InputComponent from './components/InputComponent.svelte';
import StatusBarComponent from './components/StatusBarComponent.svelte';
import DrawTextComponent from './components/DrawTextComponent.svelte';
import InteractionMenuComponent from './components/InteractionMenuComponent.svelte';
import CircleGame from './games/CircleGame.svelte';

EventHandler();
document.onkeyup = handleKeyUp;

if(isDevMode) {
notificationMock();
const memoryGameData = {
game: GamesEnum.Memory,
gameName: 'Memory MiniGame',
gameDescription: 'Lorem ipsum is placceholder text commonly used in the arachic, print and publishing industries for previewing layouts and visual mockups.',
amountOfAnswers: 9,
gameTime: 30, // seconds
maxAnswersIncorrect: 0,
displayInitialAnswersFor: 5, //seconds
gridSize: 10, //5,6,7,8,9,10 - one of these values as number of rows and columns
triggerEvent: 'memorygame-callback',
};
// setupGame({ data: memoryGameData});

const scramblerGameData = {
game: GamesEnum.Scrambler,
gameName: 'Scrambler MiniGame',
gameDescription: 'Lorem ipsum is placeholder text commonly used in the arachic, print and publishing industries for previewing layouts and visual mockups.',
amountOfAnswers: 4, // count of numbers to display
gameTime: 80, // seconds
sets: 'numeric', // numeric, alphabet, alphanumeric, greek, braille, runes
changeBoardAfter: 3 //seconds
};
// setupGame({ data: scramblerGameData});

const numberMazeGameData = {
game: GamesEnum.NumberMaze,
gameName: 'Number Maze MiniGame',
gameDescription: 'Lorem ipsum is placeholder text commonly used in the arachic, print and publishing industries for previewing layouts and visual mockups.',
gameTime: 30, // seconds
maxAnswersIncorrect: 2,
triggerEvent: 'maze-callback',
};
// setupGame({ data: numberMazeGameData});

const numberPuzzleGameData = {
game: GamesEnum.NumberPuzzle,
gameName: 'Number Puzzle MiniGame',
gameDescription: 'Lorem ipsum is placeholder text commonly used in the arachic, print and publishing industries for previewing layouts and visual mockups.',
gameTime: 3, // seconds
maxAnswersIncorrect: 2,
amountOfAnswers: 5,
timeForNumberDisplay: 5, // seconds
triggerEvent: 'var-callback',
};
// setupGame({ data: numberPuzzleGameData});

//input
const inputData = [
{
id: '1',
label: 'Name',
icon: 'fa-solid fa-pen',
placeholder: 'Insert name',
type: 'text',
},
{
id: '2',
label: 'Password',
icon: 'fa-solid fa-lock',
placeholder: 'Enter password',
type: 'password',
},
{
id: '3',
label: 'Phone',
icon: 'fa-solid fa-phone',
placeholder: 'Enter phone number',
type: 'phone',
},
];

// showInput(inputData);

const statusBarData = {
title: 'Area Dominance',
description: 'Write some description here',
icon: 'fa-solid fa-circle-info',
items: [
{
key: 'Gang', value: 'Ballas'
},
{
key: 'Dominance', value: '20%'
}
],
};
// showStatusBar(statusBarData);

// // double call for status bar
// setTimeout(() => {
// showStatusBar(
// {
// title: 'Area Check',
// description: 'Whats up',
// icon: 'fa-solid fa-heart',
// items: [
// {
// key: 'Gang', value: 'Ace'
// }
// ],
// }
// )
// }, 5000);

const drawTextData = {
icon: 'fa-solid fa-circle-info',
keys: 'Press [E] to interact',
color: 'yellow'
};
// showDrawTextMenu(drawTextData);

// // double call for draw text
// setTimeout(() => {
// showDrawTextMenu(
// {
// icon: 'fa-solid fa-circle-info',
// keys: 'Press [E] to interact and check if the old one exists',
// color: ''
// }
// );
// }, 5000);

const interactionMenuData = [
{
header: 'Menu item 1',
text: 'Some text',
icon: 'fa-solid fa-user',
color: '#02f1b5',
callback: '',
subMenu: null,
},
{
header: 'Menu item 2',
icon: 'fa-solid fa-user',
color: '',
callback: '',
subMenu: [
{
header: 'Submenu1',
icon: 'fa-solid fa-circle-info',
color: '#02f1b5',
},
{
header: 'Submenu2',
icon: 'fa-solid fa-circle-info',
color: '#02f1b5',
},
],
},
];
// setupInteractionMenu(interactionMenuData);
}
</script>

{#if $showUi }
<!-- class="min-h-screen min-w-full" -->
<main transition:fade={{ duration: 100 }} class="main-bg">
{#if $showComponent === UIComponentsEnum.StatusBar}
<StatusBarComponent />
{/if}
{#if $showComponent === UIComponentsEnum.DrawText}
<DrawTextComponent />
{/if}
{#if $showComponent === UIComponentsEnum.Menu}
<InteractionMenuComponent />
{/if}
{#if $showComponent === UIComponentsEnum.Input}
<InputComponent />
{/if}
{#if $showComponent === UIComponentsEnum.Game}
<GameLauncher />
{/if}
{#if $showComponent === UIComponentsEnum.Image}
<Image />
{/if}
{#if $showComponent === UIComponentsEnum.Notification}
<Notification />
{/if}
<CircleGame />
</main>
{/if}

<style>
.main-bg {
height: 100%;
width: 100%;
overflow: hidden;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,40 @@
<script lang="ts">
export let color: string = '';
</script>

<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 437.3 512"
style="enable-background:new 0 0 437.3 512;"
xml:space="preserve"
fill={color}
>
<g>
<path
d="M230.3,455.7c-8.3,0-16.6,0-25,0c-8.5-3.1-17.2-5.7-25.4-9.4c-21.9-9.8-42.1-22.2-58.1-40.4c-10-11.3-16.2-24.2-15.6-39.9
c0.5-13.5,0.1-27,0.1-40.5c0-2-0.2-3.2-2.5-4.1c-24.3-9.4-38-29.4-38.1-55.6c-0.1-24.7-0.3-49.4,0.1-74.1c0.2-9.7,0.9-19.6,3.2-29
c12.1-47.6,42.8-78.7,87.9-96.4c21.1-8.3,43.3-10.3,65.8-10c33.5,0.4,64.5,8.9,91.8,28.7c35.8,26,56,60.8,56.8,105.6
c0.5,25.3,0.2,50.7,0,76c-0.2,25.5-14.4,45.8-37.8,54.6c-2.4,0.9-2.8,2.1-2.8,4.3c0.1,14.3-0.1,28.6,0.1,42.9
c0.2,11.1-3.6,20.9-9.8,29.8c-10,14.4-23.5,24.8-38.1,34C266.6,442.5,249.2,450.7,230.3,455.7z M165.8,219.1
c-4.7,0.4-9.3,0.5-14,1.1c-12.7,1.5-22,8-26.3,20.1c-4.1,11.4-2.3,22.9,2.4,33.7c3.9,8.9,13.3,13.7,22.6,10.8c9-2.8,17.7-7.2,26-12
c8.6-5,15.2-12.4,19.6-21.6c5.8-12,2.3-22.3-9.5-28.3C180,219.5,173,218.9,165.8,219.1z M274.1,218.4c-5.8,1-11.9,1.4-17.5,3.2
c-14.4,4.7-19.2,16.8-12.3,30.2c1.7,3.3,3.8,6.5,6.2,9.4c9.1,11.3,21.8,17.4,34.8,22.6c11.9,4.8,24.3-0.9,28.1-13.1
c1.9-6.2,2.5-13,2.9-19.5c0.7-12.6-7.2-24.7-19.2-28.7C289.9,220.2,282,219.7,274.1,218.4z M212.6,371.3c-0.5-6.4-2.3-9-6.2-9.2
c-3.9-0.2-5.8,2.4-6.9,8.9c-4,0-7.9,0-12.1,0c0.3-4.2-0.8-7.4-4.8-8.6c-4.1-1.2-6.5,1.3-8.5,4.6c-3.7-2-5.4-4.9-5.4-8.9
c0-5.8,0-11.7,0-17.5c0-6.9-0.8-8.1-7.2-10.2c-9.7-3.3-19.4-6.5-29.2-9.7c-1.3-0.4-2.7-0.9-4.1-1c-3-0.2-5.1,1.3-6,4
c-1,2.7-0.2,5.2,2.2,6.9c1.2,0.9,2.8,1.4,4.2,1.9c8.4,2.8,16.7,5.7,25.1,8.4c2,0.6,2.6,1.6,2.6,3.6c-0.1,4.7,0,9.4,0,14
c0,7,2.8,12.6,8.2,16.9c8.1,6.6,17.7,8.5,27.7,8.5c17,0.1,34.1,0.3,51.1-0.1c6.8-0.2,13.7-1.2,20.3-3c9.4-2.7,15.9-8.8,16.8-19.2
c0.5-5.4,0.7-10.9,0.6-16.3c0-2.6,0.7-3.9,3.3-4.7c8.7-2.7,17.2-5.7,25.8-8.8c4.8-1.7,6.3-6.8,3.1-10.2c-2.4-2.6-5.3-2.2-8.3-1.2
c-10.3,3.5-20.6,7-31,10.4c-3.8,1.2-5.4,3.6-5.3,7.5c0.1,6,0.2,12,0,17.9c-0.1,4.8-1.2,9.3-6.5,11.3c-1.1-3.4-3.1-5.9-6.7-5.4
c-5.1,0.6-5.4,4.8-5.7,9c-4.2,0-8.1,0-12.1,0c-0.5-6.3-2.3-8.9-6.2-9.1c-3.8-0.2-5.8,2.4-6.9,9.2
C220.6,371.3,216.7,371.3,212.6,371.3z M218,331C218,331,218,331,218,331c3.9,0,7.8,0.2,11.7-0.1c5.6-0.3,8.7-3,9.4-8.6
c0.4-3.4,0.3-7-0.1-10.5c-1.6-11.9-4.4-23.5-11.1-33.7c-5.3-8.2-13.3-8.9-18.6-1.1c-9,13.3-12.5,28.4-12.3,44.3
c0.1,5.9,3.9,9.2,9.8,9.5C210.4,331.2,214.2,331,218,331z"
/>
</g>
</svg>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 570 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 858 B

View file

@ -0,0 +1,229 @@
<script lang="ts">
import { onMount } from "svelte";
import Icon from "./Icon.svelte";
import { isDevMode, showComponent, showUi } from "../stores/GeneralStores";
import { drawTextStore, hideDrawTextMenu, hideDrawTextStore } from "../stores/DrawTextStore";

let drawTextDataValue:any = {};
drawTextStore.subscribe((value) => drawTextDataValue = value);

let hideDrawTextValue = false;
hideDrawTextStore.subscribe(value => {
hideDrawTextValue = value;
});

let marginLeftVal = '2%';
let interactionText = [], colorValue = '';
onMount(() => {
getColorValue();
handleInteractionText();

if(isDevMode) {
setTimeout(() => {
hideDrawTextMenu();
}, 10000);
}
});

function getColorValue() {
switch(drawTextDataValue.color) {
case "primary":
colorValue ="#0275d8";
break;
case "error":
colorValue = "#d9534f";
break;
case "success":
colorValue = "#5cb85c";
break;
case "warning":
colorValue = "#f0ad4e";
break;
case "info":
colorValue = "#5bc0de";
break;
case "mint":
colorValue = "#a1f8c7";
break;
default:
colorValue = "var(--color-green)";
break;
}
}

function handleInteractionText() {
let matches = drawTextDataValue.keys.match(/\[(.*?)\]/);
if (matches) {
let submatch = matches[0];

let splitText = drawTextDataValue.keys.split(submatch);
interactionText = [
{
value: splitText[0],
color: 'var(--color-lightgrey)'
},
{
value: submatch,
color: colorValue
},
{
value: splitText[1],
color: 'var(--color-lightgrey)'
}
];
} else {
interactionText = [
{
value: drawTextDataValue.keys,
color: 'var(--color-lightgrey)'
}
];
}
}

function closeDrawText() {
const statusWrapperDom = document.getElementById('draw-text-wrapper');
if(statusWrapperDom) {
statusWrapperDom.style.animation = '2s slide-left';

let keyFrames = document.createElement('style');
keyFrames.innerHTML = `
@keyframes slide-left {
from {
margin-left: `+marginLeftVal+`;
}
to {
margin-left: -20%;
}
}

.draw-text-wrapper {
-moz-animation: 2s slide-left;
-webkit-animation: 2s slide-left;
animation: 2s slide-left;
}
`;

statusWrapperDom.appendChild(keyFrames);

setTimeout(() => {
showUi.set(false);
showComponent.set(null);
drawTextStore.set({
// title: '',
icon: '',
keys: '',
color: ''
});
hideDrawTextStore.set(false);
}, 500);
}
}

$: {
if(hideDrawTextValue) {
closeDrawText();
}

if(drawTextDataValue) {
handleInteractionText();
}
}
</script>

<div id="draw-text-wrapper" class="draw-text-wrapper" style="margin-left:{marginLeftVal};">
<div class="draw-text-title-wrapper">
<div class="icon">
<Icon icon={drawTextDataValue.icon} styleColor={colorValue} />
</div>

<div class="title-info">
<p class="title-description">
{#each interactionText as text}
<p style="color: {text.color};">{text.value}</p>
{/each}
</p>
</div>
</div>
</div>

<style>
.draw-text-wrapper {
-moz-animation: 1s slide-right;
-webkit-animation: 1s slide-right;
animation: 1s slide-right;
position: absolute;
top: 50%;
transform: translateY(-50%);

min-width: 10vw;
width: max-content;

min-height: 3vw;
height: fit-content;

overflow: hidden;

background-color: var(--color-darkblue);
border-radius: 0.3vw;

padding: 0.75vw 1.5vw;
display: flex;
flex-direction: column;
}
@keyframes slide-right {
from {
margin-left: -20%;
}
to {
margin-left: 2%;
}
}
@keyframes slide-left {
from {
margin-left: 2%;
}
to {
margin-left: -100%;
}
}

.draw-text-wrapper > .draw-text-title-wrapper {
display: flex;
flex-direction: row;
}

.draw-text-wrapper > .draw-text-title-wrapper > .icon {
width: 1.5vw;
margin: auto 0.75vw auto 0;

font-size: 1.25vw;
}
.draw-text-wrapper > .draw-text-title-wrapper > .title-info {
display: flex;
flex-direction: column;
text-transform: capitalize;
}

.draw-text-wrapper > .draw-text-title-wrapper > .title-info > .title-description {
font-size: 1vw;
font-weight: 300;
color: var(--color-lightgrey);

display: flex;
flex-direction: row;
word-wrap: break-word;
flex-wrap: wrap;

margin-top: auto;
margin-bottom: auto;
}
.draw-text-wrapper > .draw-text-title-wrapper > .title-info > .title-description > p {
margin-right: 0.25vw;
}
</style>

View file

@ -0,0 +1,8 @@
<script lang="ts">
export let icon: string;
export let color: string = '';
export let classes: string = '';
export let styleColor: string = '';
</script>

<i class="{color} {icon} {classes}" style="color: {styleColor};" />

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { imageStore } from './../stores/ImageStore';
import { fly } from 'svelte/transition';
</script>

{#if $imageStore.show}
<div class="flex items-center justify-center min-h-screen" transition:fly="{{y: +400}}">
<div class="flex items-center flex-col ps-bg-darkblue p-10 shadow-md shadow-gray-800 rounded-md">
<!-- svelte-ignore a11y-img-redundant-alt -->
<img src={$imageStore.url} alt="Image Placeholder" />
</div>
</div>
{/if}

View file

@ -0,0 +1,203 @@
<script lang="ts">
import { hideUi, isDevMode } from "../stores/GeneralStores";
import { inputStore } from "../stores/InputStores";
import Icon from "./Icon.svelte";
import { onMount } from "svelte";
import fetchNui from "../../utils/fetch";
import { handleKeyUp } from "../../utils/eventHandler";

document.onkeyup = handleKeyUp;
let inputData:any = $inputStore;

onMount(() => {
inputData = inputData.map((val) => {
val.value = null;
return val;
});
});

function submit() {
let returnData = [];

let inputs = document.querySelectorAll('input');
inputs.forEach((input: HTMLInputElement, index) => {
let returnObj = {
id: input.id,
value: input.value,
// compIndex: index
};
returnData.push(returnObj)
});

if(!isDevMode) {
fetchNui('input-callback', returnData);
}

closeInputs();
}

export function closeInputs(): void {
let inputs = document.querySelectorAll('input');
inputs.forEach((input: HTMLInputElement) => {
input.value = '';
});

inputData = [];

if(!isDevMode) {
fetchNui('input-close', { ok: true });
}
hideUi();
}
</script>

<div class="input-base-wrapper">
<div class="logo">
<img src="./images/ps-logo.png" alt="ps-logo" />
</div>

<div class="input-form">
{#each inputData as inputValue}
<div class="input-wrapper">
<div class="input-data-wrapper">
<div class="input-icon">
<Icon icon={inputValue.icon} color="ps-text-green" classes="text-2xl" />
</div>
<div class="input-area">
<p class="label">
{inputValue.label}
</p>
<input id={inputValue.id} type={inputValue.type} class="value" placeholder={inputValue.placeholder} value={inputValue.value} />
</div>
</div>
<div class="horizontal-line"></div>
<!-- <div class="input-message">
{inputValue.placeholder}
</div> -->
</div>
{/each}
</div>

<div class="button-wrapper">
<button class="submit-btn" on:click={submit}>Submit</button>
<button class="cancel-btn" on:click={closeInputs}>Cancel</button>
</div>
</div>

<style>
.input-base-wrapper {
/* centering the view */
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);

width: 25vw;
min-height: 28vw;
height: fit-content;

overflow: hidden;

background-color: var(--color-darkblue);
border-radius: 0.3vw;

padding: 0.5vw 2vw;
display: flex;
flex-direction: column;
}

.input-base-wrapper > .input-form {
margin-top: 1vw;
height: 100%;

/* border: 0.1px solid red; */
}

.input-base-wrapper > .input-form > .input-wrapper {
display: flex;
flex-direction: column;
}

.input-base-wrapper > .input-form > .input-wrapper > .horizontal-line {
width: inherit;
height: 1px;
background-color: var(--color-lightgrey);
}
.input-base-wrapper > .input-form > .input-wrapper > .input-message {
font-size: 0.8vw;
opacity: 0.85;
color: var(--color-lightgrey);
margin-top: 0.2vw;
text-transform: capitalize;
}
.input-base-wrapper > .input-form > .input-wrapper > .input-data-wrapper {
display: flex;
flex-direction: row;

padding: 0.5vw 0;
}
.input-base-wrapper > .input-form > .input-wrapper:not(:last-child) {
margin-bottom: 1vw;
}

.input-base-wrapper > .input-form > .input-wrapper > .input-data-wrapper > .input-icon {
margin-right: 0.75vw;
width: 1.5vw;

margin-top: auto;
margin-bottom: auto;
}
.input-base-wrapper > .input-form > .input-wrapper > .input-data-wrapper > .input-area {
display: flex;
flex-direction: column;

color: var(--color-lightgrey);
}
.input-base-wrapper > .input-form > .input-wrapper > .input-data-wrapper > .input-area > .label {
font-size: 1vw;
font-weight: 500 !important;
}
.input-base-wrapper > .input-form > .input-wrapper > .input-data-wrapper > .input-area > .value {
font-size: 0.8vw;
font-weight: 300 !important;
width: 18vw;
background-color: inherit;
margin-top: 0.2vw;
}

input:focus {
padding-left: 0;
outline: none;
border-bottom-width: 2px;
border-bottom-color: var(--color-green);
margin-bottom: -1px;
}

.input-base-wrapper > .button-wrapper {
display: flex;
flex-direction: row;
justify-content: space-evenly;

margin: 1.5vw auto 1.25vw auto;
color: var(--color-black);
}
.input-base-wrapper > .button-wrapper > .submit-btn {
background-color: var(--color-green);
}
.input-base-wrapper > .button-wrapper > .cancel-btn {
background-color: var(--color-darkgrey);
}

button {
border-radius: 0.3vw;
padding: 0.3vw 1vw;
text-transform: uppercase;
font-weight: 500 !important;
}
button:not(:last-child) {
margin-right: 0.5vw;
}
</style>

View file

@ -0,0 +1,256 @@
<script lang="ts">
import { closeInteractionMenu, menuStore } from "../stores/MenuStores";
import Icon from "./Icon.svelte";
import fetchNui from "../../utils/fetch";
import { isDevMode } from "../stores/GeneralStores";

let menuData:Array<any> = $menuStore;
let selectedMenuItem = null;
let subMenu = null;

let subMenuTextColorOverride = {
id: null, color: 'black'
};
let menuTextColorOverride = {
id: null, color: 'black'
};

function handleMenuSelection(selectedMenu) {
selectedMenuItem = selectedMenu;

if(selectedMenu) {
if(!selectedMenu.subMenu && !isDevMode) {
fetchNui('MenuSelect', {data :selectedMenu});
closeInteractionMenu();
} else {
subMenu = selectedMenuItem.subMenu;
}
}
}

function handleSubMenuSelection(selectedSubMenu) {
const formData = {
data:selectedSubMenu
};
if(!isDevMode) {
fetchNui('MenuSelect', formData);
closeInteractionMenu();
}
}

function handleItemHover(itemId, index, color, action='enter', isSubMenu = false) {
const itemDom = document.getElementById(itemId);
if(action === 'enter') {
if(isSubMenu) {
itemDom.style.backgroundColor = color || 'var(--color-green)';
subMenuTextColorOverride.id = index;
} else {
itemDom.style.backgroundColor = color || 'var(--color-green)';
menuTextColorOverride.id = index;
}
} else {
itemDom.style.backgroundColor = 'var(--color-darkblue)';
subMenuTextColorOverride.id = null;
menuTextColorOverride.id = null;
}
}
</script>

<div class="menu-base-wrapper">
<div class="screen-base">
{#if !selectedMenuItem}
<div class="header-slot" style="border: 3px solid var(--color-green);">
<img src="./images/ps-logo.png" alt="ps-logo" />
</div>

<div class="screen-body">
{#each menuData as menu, index}
<div id={"menu-"+index} class="each-panel"
on:mouseenter={() => handleItemHover("menu-"+index, index, menu.color, 'enter', false)}
on:mouseleave={() => handleItemHover("menu-"+index, index, menu.color, 'leave', false)}
on:click={() => handleMenuSelection(menu)}>
<div class="menu-icon">
<Icon icon={menu.icon} styleColor={menuTextColorOverride.id === index ? menuTextColorOverride.color : menu?.color || 'var(--color-green)'} />
</div>
<div class="menu-details">
<p class="header" style="color: {menuTextColorOverride.id === index ? menuTextColorOverride.color : 'var(--color-white)'};">{menu.header}</p>
{#if menu.hasOwnProperty('text')}
<p class="text" style="color: {menuTextColorOverride.id === index ? menuTextColorOverride.color : 'var(--color-lightgrey)'};">{menu.text}</p>
{/if}
</div>
{#if menu?.subMenu}
<div class="chevron" style="color: {menuTextColorOverride.id === index ? menuTextColorOverride.color : 'var(--color-white)'};">
<i class="fa-solid fa-chevron-right"></i>
</div>
{/if}
</div>
{/each}
</div>
{:else if selectedMenuItem.hasOwnProperty('subMenu') && selectedMenuItem.subMenu}
<div class="submenu-header-slot" on:click={() => handleMenuSelection(null)}>
<i class="fa-solid fa-chevron-left left-chevron"></i>
<p class="main-menu">Main Menu</p>
</div>

<div class="screen-body">
{#each subMenu as menu, index}
<div id={"sub-menu-"+index} class="each-panel"
on:mouseenter={() => handleItemHover("sub-menu-"+index, index, menu.color, 'enter', true)}
on:mouseleave={() => handleItemHover("sub-menu-"+index, index, menu.color, 'leave', true)}
on:click={() => handleSubMenuSelection(menu)}>
<div id={"menu-icon-"+index} class="menu-icon">
<Icon icon={menu.icon} styleColor={subMenuTextColorOverride.id === index ? subMenuTextColorOverride.color : menu.color || 'var(--color-green)'} />
</div>
<div class="menu-details">
<p class="header" style="color: {subMenuTextColorOverride.id === index ? subMenuTextColorOverride.color : 'var(--color-white)'};">{menu.header}</p>
{#if menu.hasOwnProperty('text')}
<p class="text" style="color: {subMenuTextColorOverride.id === index ? subMenuTextColorOverride.color : 'var(--color-lightgrey)'};">{menu.text}</p>
{/if}
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>

<style>
.menu-base-wrapper {
/* centering the view */
position: absolute;
left: 70%;
top: 40%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);

width: 20vw;
height: 30vw;
/* min-height: 4vw;
height: fit-content; */

overflow: hidden;
/* border: 0.1px solid red; */
}

.menu-base-wrapper > .screen-base {
height: 100%;
/* border: 0.1px solid yellow; */

display: flex;
flex-direction: column;
}

.menu-base-wrapper > .screen-base > .header-slot {
background-color: var(--color-darkblue);
border-radius: 0.3vw;

padding: 0.5vw 2vw;
display: flex;
flex-direction: row;

min-height: 3vw;
height: fit-content;
}
.menu-base-wrapper > .screen-base > .header-slot > img {
height: 4vw;
align-self: center;
}

.menu-base-wrapper > .screen-base > .submenu-header-slot {
min-height: 4vw;
height: max-content;

background-color: var(--color-darkblue);
border-radius: 0.3vw;
color: var(--color-white);

padding: 0.3vw 1vw;
cursor: pointer;

display: flex;
flex-direction: row;
}
.menu-base-wrapper > .screen-base > .submenu-header-slot > .left-chevron {
margin-top: auto;
margin-bottom: auto;
margin-right: 0.5vw;

width: 1.5vw;
font-size: 1.1vw;
}
.menu-base-wrapper > .screen-base > .submenu-header-slot > .main-menu {
font-weight: 500;
font-size: 1.25vw;

margin-top: auto;
margin-bottom: auto;
}

.menu-base-wrapper > .screen-base > .screen-body {
margin-top: 0.3vw;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
/* border: 0.1px solid blue; */
}

.menu-base-wrapper > .screen-base > .screen-body > .each-panel {
min-height: 4vw;
height: max-content;

background-color: var(--color-darkblue);
border-radius: 0.3vw;

padding: 0.5vw 1vw;
cursor: pointer;

display: flex;
flex-direction: row;
}
.menu-base-wrapper > .screen-base > .screen-body > .each-panel:not(:last-child) {
margin-bottom: 0.3vw;
}

.menu-base-wrapper > .screen-base > .screen-body > .each-panel > .menu-icon {
margin-top: auto;
margin-bottom: auto;
margin-right: 0.5vw;

width: 1.5vw;
font-size: 1vw;
}

.menu-base-wrapper > .screen-base > .screen-body > .each-panel > .menu-details {
display: flex;
flex-direction: column;
margin-top: auto;
margin-bottom: auto;
}

.menu-base-wrapper > .screen-base > .screen-body > .each-panel > .menu-details > .header {
font-size: 0.8vw;
white-space: nowrap;
/* color: var(--color-white); */
}

.menu-base-wrapper > .screen-base > .screen-body > .each-panel > .menu-details > .text {
font-size: 0.6vw;
white-space: nowrap;
/* color: var(--color-lightgrey); */
}

.menu-base-wrapper > .screen-base > .screen-body > .each-panel > .chevron {
margin-left: 72%;
font-size: 1vw;
/* color: var(--color-white); */

margin-top: auto;
margin-bottom: auto;
}
</style>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { notifications } from './../stores/NotificationStore';
import { NotificationIcons, NotificationTypes } from './../enums/NotificationTypesEnum';
</script>

<div class="flex justify-end min-h-screen">
<div class="flex flex-col mr-4 mt-4 w-[200px]">
{#each $notifications as notification}
<div
class="{notification.type} flex items-center px-4 py-3 mb-1 text-white min-w-full rounded"
>
<div class="text-2xl mt-1">
{#if notification.type === NotificationTypes.Success}
<i class={NotificationIcons.Success} />
{:else if notification.type === NotificationTypes.Warning}
<i class={NotificationIcons.Warning} />
{:else if notification.type === NotificationTypes.Error}
<i class={NotificationIcons.Error} />
{:else if notification.type === NotificationTypes.Info}
<i class={NotificationIcons.Info} />
{/if}
</div>
<span class="ml-3">{notification.text}</span>
</div>
{/each}
</div>
</div>

View file

@ -0,0 +1,195 @@
<script lang="ts">
import { hideStatusBar, hideStatusBarStore, statusBarStore } from "../stores/StatusBarStores";
import { onMount } from "svelte";
import Icon from "./Icon.svelte";
import type { IStatusBarItem } from "src/interfaces/IStatusBar";
import { isDevMode, showComponent, showUi } from "../stores/GeneralStores";

let statusData:any = $statusBarStore;
let statusDataItems:[IStatusBarItem] = statusData.items;

statusBarStore.subscribe((value) => {
statusData = value;
statusDataItems = statusData.items;
});

let hideStatusBarValue = false;
hideStatusBarStore.subscribe(value => {
hideStatusBarValue = value;
});

onMount(() => {
if(isDevMode) {
setTimeout(() => {
hideStatusBar();
}, 10000);
}
});

function closeStatusBar() {
const statusWrapperDom = document.getElementById('status-bar-wrapper');
if(statusWrapperDom) {
statusWrapperDom.style.animation = '2s hide-statusbar';

let keyFrames = document.createElement('style');
keyFrames.innerHTML = `
@keyframes hide-statusbar {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

.status-bar-wrapper {
-moz-animation: 2s hide-statusbar;
-webkit-animation: 2s hide-statusbar;
animation: 2s hide-statusbar;
}
`;

statusWrapperDom.appendChild(keyFrames);

setTimeout(() => {
showUi.set(false);
showComponent.set(null);
statusBarStore.set({
title: '',
description: '',
items: [],
icon: '',
});
hideStatusBarStore.set(false);
}, 500);
}
}

$: {
if(hideStatusBarValue) {
// if(hideStatusBarValue || !hideStatusBarValue)
closeStatusBar();
}
}
</script>

<div id="status-bar-wrapper" class="status-bar-wrapper">
<div class="status-title-wrapper">
<div class="icon">
<Icon icon={statusData.icon} color="ps-text-green" />
</div>

<div class="title-info">
<p class="title">
{statusData.title}
</p>
<p class="title-description">
{statusData.description}
</p>
</div>
</div>

<div class="items-wrapper">
{#each statusDataItems as item}
<div class="each-item">
<p class="label">
{item.key}:
</p>
<p class="value">
{item.value}
</p>
</div>
{/each}
</div>
</div>

<style>
.status-bar-wrapper {
-moz-animation: 2s display-status;
-webkit-animation: 2s display-status;
animation: 2s display-status;

position: absolute;
left: 50%;
bottom: 1%;
transform: translateX(-50%);

width: 23vw;
min-height: 8vw;
height: fit-content;

overflow: hidden;

background-color: var(--color-darkblue);
border-radius: 0.3vw;

padding: 1vw 2vw;
display: flex;
flex-direction: column;
}
@keyframes display-status {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

.status-bar-wrapper > .status-title-wrapper {
display: flex;
flex-direction: row;
}

.status-bar-wrapper > .status-title-wrapper > .icon {
width: 1.5vw;
margin-right: 0.5vw;

font-size: 1.25vw;
}
.status-bar-wrapper > .status-title-wrapper > .title-info {
display: flex;
flex-direction: column;
text-transform: capitalize;
}
.status-bar-wrapper > .status-title-wrapper > .title-info > .title {
font-size: 1.3vw;
font-weight: 500;
color: var(--color-white);
}
.status-bar-wrapper > .status-title-wrapper > .title-info > .title-description {
font-size: 0.95vw;
font-weight: 200;
color: var(--color-lightgrey);

margin-top: -0.2vw;
}

.status-bar-wrapper > .items-wrapper {
margin-left: 2vw;
margin-top: 0.5vw;
}
.status-bar-wrapper > .items-wrapper > .each-item {
display: flex;
flex-direction: row;

word-wrap: break-word;
flex-wrap: wrap;

font-size: 0.95vw;
}
.status-bar-wrapper > .items-wrapper > .each-item:not(:last-child) {
margin-bottom: 0.3vw;
}

.status-bar-wrapper > .items-wrapper > .each-item > .label {
color: var(--color-lightgrey);
}
.status-bar-wrapper > .items-wrapper > .each-item > .value {
color: var(--color-green);
margin-left: 0.3vw;
}
</style>

View file

@ -0,0 +1,4 @@
export enum ConnectingGameMessageEnum {
Connecting = 'CONNECTING TO INTERFACE',
Connected = 'CONNECTED. GET READY.',
}

View file

@ -0,0 +1,6 @@
export enum GamesEnum {
Scrambler = 'Scramber',
NumberMaze = 'NumberMaze',
Memory = 'MemoryGame',
NumberPuzzle = 'NumberPuzzle',
}

View file

@ -0,0 +1,13 @@
export enum NotificationTypes {
Success = 'ps-notification-success',
Error = 'ps-notification-error',
Warning = 'ps-notification-warning',
Info = 'ps-notification-info',
}

export enum NotificationIcons {
Success = 'fa-solid fa-circle-check',
Error = 'fa-solid fa-circle-exclamation',
Warning = 'fa-solid fa-triangle-exclamation',
Info = 'fa-solid fa-circle-info',
}

View file

@ -0,0 +1,11 @@
export enum UIComponentsEnum {
StatusBar = 'StatusBar',
Menu = 'Menu',
Input = 'Input',
Game = 'Game',
MemoryGame = 'MemoryGame',
Image = 'ShowImage',
DrawText = 'DrawText',
Notification = 'Notify',
None = 'hideUi',
}

View file

@ -0,0 +1,139 @@
<script lang="ts">
import { onMount } from "svelte";
let canvas;
let ctx;
let W, H, degrees = 0, new_degrees = 0, color = "#38D5AF";
let txtcolor = "#ffffff", bgcolor = "#2B312B", bgcolor2 = "#068f6d", bgcolor3 = "#00ff00";
let key_to_press, g_start, g_end, animation_loop;
let needed = 4, streak = 0, circle_started = false, time = 2;

function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}

function StartCircle() {
ctx.clearRect(0, 0, W, H);
ctx.beginPath();
ctx.strokeStyle = bgcolor;
ctx.lineWidth = 20;
ctx.arc(W / 2, H / 2, Math.min(W, H) / 2 - 10, 0, Math.PI * 2, false);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = correct === true ? bgcolor3 : bgcolor2;
ctx.lineWidth = 20;
ctx.arc(W / 2, H / 2, Math.min(W, H) / 2 - 10, g_start - 90 * Math.PI / 180, g_end - 90 * Math.PI / 180, false);
ctx.stroke();
let radians = degrees * Math.PI / 180;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 40;
ctx.arc(W / 2, H / 2, Math.min(W, H) / 2 - 20, radians - 0.1 - 90 * Math.PI / 180, radians - 90 * Math.PI / 180, false);
ctx.stroke();
ctx.fillStyle = txtcolor;
ctx.font = "100px sans-serif";
let text_width = ctx.measureText(key_to_press).width;
ctx.fillText(key_to_press, W / 2 - text_width / 2, H / 2 + 35);
}

function draw() {
if (typeof animation_loop !== undefined) clearInterval(animation_loop);
g_start = getRandomInt(20, 40) / 10;
g_end = getRandomInt(5, 10) / 10;
g_end = g_start + g_end;
degrees = 0;
new_degrees = 360;
key_to_press = '' + getRandomInt(1, 4);
animation_loop = setInterval(animate_to, time);
}

function animate_to() {
if (degrees >= new_degrees) {
CircleFail();
return;
}
degrees += 2;
StartCircle();
}

function correct() {
streak += 1;
if (streak == needed) {
clearInterval(animation_loop);
endGame(true);
} else {
draw();
}
}

function CircleFail() {
clearInterval(animation_loop);
endGame(false);
}

function startGame() {
document.getElementById('circle').style.display = 'block';
document.getElementById('circle').style.pointerEvents = 'auto';
circle_started = true;
draw();
}

function endGame(status) {
document.getElementById('circle').style.display = 'none';
circle_started = false;
let endResult = status ? true : false;
fetch(`https://ps-ui/circle-result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({endResult})
});
streak = 0;
needed = 4;
}

export function setupCircleGame(data: { circles?: number; time?: number }) {
needed = data.circles ?? 4;
time = data.time ?? 2;
startGame();
}

onMount(() => {
canvas = document.getElementById("circle");
ctx = canvas.getContext("2d");
W = window.innerWidth * 0.2;
H = window.innerHeight * 0.2;
canvas.width = W;
canvas.height = H;

window.addEventListener("message", (event) => {
if (event.data.action == "circle-start") {
needed = event.data.circles ?? 4;
time = event.data.time ?? 2;
startGame();
}
});

document.addEventListener("keydown", function (ev) {
let key_pressed = ev.key;
let valid_keys = ['1', '2', '3', '4'];
if (valid_keys.includes(key_pressed) && circle_started) {
if (key_pressed === key_to_press) {
let d_start = (180 / Math.PI) * g_start;
let d_end = (180 / Math.PI) * g_end;
if (degrees < d_start || degrees > d_end) {
CircleFail();
} else {
correct();
}
} else {
CircleFail();
}
}
});
});
</script>

<div class="absolute inset-0 flex items-center justify-center" style="pointer-events: none; z-index: 100;">
<canvas id="circle" class="w-auto h-auto" style="display: none;"></canvas>
</div>

View file

@ -0,0 +1,104 @@
<script lang="ts">
import { currentActiveGameDetails, currentGameActive } from "../stores/GameLauncherStore";
import Skull from "../assets/svgs/Skull.svelte";
import { GamesEnum } from "../enums/GamesEnum";
import NewMemoryGame from "./MemoryGame.svelte";
import SuccessFailureScreen from "./SuccessFailureScreen.svelte";
import NewScrambler from "./Scrambler.svelte";
import NewNumberMaze from "./NumberMaze.svelte";
import NewNumberPuzzle from "./NumberPuzzle.svelte";

const skullColor: string = 'var(--color-green)';

let showResultScreen = false, hackSuccess = false;
</script>

{#if !showResultScreen}
<div class="games-container">
<div class="game-wrapper ps-bg-darkblue">
<div class="skull-logo">
<Skull color={skullColor} />
</div>

<div class="game-heading">
<p class="ps-font-arcade">
{$currentActiveGameDetails?.gameName}
</p>
</div>

<div class="game-description">
<p>{$currentActiveGameDetails?.gameDescription}</p>
</div>

<div class="main-game-body">
{#if $currentGameActive === GamesEnum.Memory}
<NewMemoryGame on:game-ended={(event) => {showResultScreen = true; hackSuccess = event.detail.hackSuccess; }} />
{:else if $currentGameActive === GamesEnum.Scrambler}
<NewScrambler on:game-ended={(event) => {showResultScreen = true; hackSuccess = event.detail.hackSuccess; }} />
{:else if $currentGameActive === GamesEnum.NumberMaze}
<NewNumberMaze on:game-ended={(event) => {showResultScreen = true; hackSuccess = event.detail.hackSuccess; }} />
{:else if $currentGameActive === GamesEnum.NumberPuzzle}
<NewNumberPuzzle on:game-ended={(event) => {showResultScreen = true; hackSuccess = event.detail.hackSuccess; }} />
{/if}
</div>
</div>
</div>
{:else}
<SuccessFailureScreen isSuccess={hackSuccess} />
{/if}

<style>
.games-container {
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
border-radius: 0.2vw;
}

.games-container > .game-wrapper {
width: 33vw;
height: 45vw;
border-radius: 0.3vw;
overflow: hidden;

display: flex;
flex-direction: column;
}

.games-container > .game-wrapper > .skull-logo {
margin: 0 auto;
padding-top: 1vw;
width: 5vw;
}

.games-container > .game-wrapper > .game-heading {
font-size: 1.1vw;
letter-spacing: 0.4vw;
color: var(--color-lightgrey);

margin: 0.25vw auto;

width: 19vw;
text-align: center;
}

.games-container > .game-wrapper > .game-description {
font-size: 0.7vw;
font-weight: normal;
color: var(--color-lightgrey);
text-align: center;

width: 33vw;
margin: 1vw auto;
}

.games-container > .game-wrapper > .main-game-body {
margin: 0.75vw auto 2vw auto;
width: 31vw;
/* border: 0.1px solid yellow; */
height: 30vw;
}
</style>

View file

@ -0,0 +1,79 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import Skull from '../assets/svgs/Skull.svelte';
('./../../utils/mockEvent');
import { connectionText, showLoading } from '../stores/GameLauncherStore';
import { ConnectingGameMessageEnum } from '../enums/GameConnectionMessages';
import GameBase from './GameBase.svelte';

const skullColor: string = '#02f1b5';

let loadingBar: HTMLDivElement;
let gameLoaded = false;

/** Asynchronously connects to a game and resolves the Promise when completed.
* Uses a loading bar to show progress, incrementing by 1% every 30ms until it reaches 100%.
*
* @returns Promise that resolves when the loading bar reaches 100%.
*/
async function connectToGame(): Promise<void> {
return new Promise((resolve) => {
let width = 0;
let interval = setInterval(() => {
width++;
loadingBar.style.width = `${width}%`;

if (width === 100) {
clearInterval(interval);
resolve();
}
}, 30);
});
}

// Call init() on mount
onMount(async () => {
// Show a loading indicator while connecting to the game server
connectionText.set(ConnectingGameMessageEnum.Connecting);
showLoading.set(true);

// Connect to the game server
await connectToGame();

// Hide loading indicator when completed connecting to the game server
connectionText.set(ConnectingGameMessageEnum.Connected);
setTimeout(() => {
showLoading.set(false);
gameLoaded = true;
}, 2000);
});

onDestroy(async () => {
gameLoaded = false
})
</script>

{#if $showLoading}
<div class="flex min-h-screen justify-center items-center">
<div
class="flex flex-col h-[400px] w-[700px] ps-bg-darkblue shadow-md shadow-black justify-center items-center"
>
<span class="w-40"><Skull color={skullColor} /></span>
<p class="text-white text-3xl mt-2">{$connectionText}</p>

<!-- Loading wrapper -->
<div class="flex mt-10 ps-border-green border-4 w-[80%] h-10">
<!-- Loading bar progress -->
<div
bind:this={loadingBar}
class="ps-bg-green opacity-40 will-change-auto w-0"
>
</div>
</div>
</div>
</div>
{/if}

{#if !$showLoading && gameLoaded}
<GameBase />
{/if}

View file

@ -0,0 +1,269 @@
<script lang="ts">
import { closeGame, currentActiveGameDetails, gameSettings } from "../stores/GameLauncherStore";
import { createEventDispatcher, onMount } from "svelte";
import fetchNui from "../../utils/fetch";
import { isDevMode } from "../stores/GeneralStores";

const dispatch = createEventDispatcher();

let gridSizesAcceptable = [
{
numberOfRowCol: 5,
cubeSize: '4.2vw',
gap: '1vw'
},
{
numberOfRowCol: 6,
cubeSize: '3.7vw',
gap: '0.8vw'
},
{
numberOfRowCol: 7,
cubeSize: '2.9vw',
gap: '1vw'
},
{
numberOfRowCol: 8,
cubeSize: '2.6vw',
gap: '0.9vw'
},
{
numberOfRowCol: 9,
cubeSize: '2.4vw',
gap: '0.75vw'
},
{
numberOfRowCol: 10,
cubeSize: '2.1vw',
gap: '0.75vw'
},
];

let gameTimeRemaining = 0;

let numberOfCorrectCubesToDisplay = $gameSettings.amountOfAnswers; // how many cubes to remember for game - increment number based on difficulty level
let gameTime = $gameSettings.gameTime * 100;
let numberOfWrongClicksAllowed = $gameSettings.maxAnswersIncorrect;

let correctIndices = [], displayCorrectIndicesFor = $currentActiveGameDetails.displayInitialAnswersFor * 1000; // time in seconds
let counter, gameStarted = false, gameEnded = false;
let hackSuccess = false;
let numberOfCubes = $currentActiveGameDetails.gridSize * $currentActiveGameDetails.gridSize;
let allCubes = [];

onMount(() => {
//generate random indices from number of cubes
while(correctIndices.length < numberOfCorrectCubesToDisplay){
const r = Math.floor(Math.random() * numberOfCubes);
if(correctIndices.indexOf(r) === -1) correctIndices.push(r);
}

//generating an array to maintain each cube data by index
for(let i = 0; i < numberOfCubes; i++) {
const cubeData = {
cubeIndex: i,
isCorrectAnswer: correctIndices.includes(i),
isClicked: false
};
allCubes.push(cubeData);
allCubes = allCubes;
}

let cubeWidthHeightValue = gridSizesAcceptable.filter((accept) => {
return accept.numberOfRowCol === $currentActiveGameDetails.gridSize;
})[0];

setTimeout(() => {
//assigning cube width and height
allCubes.forEach((cube) => {
const gameContainer = document.getElementById('memory-game-container');
if(gameContainer) {
gameContainer.style.gap = cubeWidthHeightValue.gap;
}
const cubeDom = document.getElementById('each-cube-'+cube.cubeIndex);
if(cubeDom) {
cubeDom.style.width = cubeWidthHeightValue.cubeSize;
cubeDom.style.height = cubeWidthHeightValue.cubeSize;
cubeDom.style.border = "2px solid var(--color-green)";
}
});
}, 1500);
//stop showing the correct cubes and start the guessing game
setTimeout(() => {
gameStarted = true;
counter = setInterval(startTimer, 10);
}, displayCorrectIndicesFor + 1500);

});

function startTimer() {
if (gameTime <= 0)
{
gameEnded = true;
hackSuccess = isSuccessful();
clearInterval(counter);
return;
}
gameTime--;
gameTimeRemaining = gameTime/100;
}

function isSuccessful() {
//all correct cubes clicked and wrong clicks are within threshold then success

//all correct cubes clicked
let allCorrectCubesClicked = false;
allCubes.map((item) => {
if(item.isCorrectAnswer && item.isClicked) {
allCorrectCubesClicked = true;
}
if(item.isCorrectAnswer && !item.isClicked) {
allCorrectCubesClicked = false;
}
});

//wrong clicks within threshold
const wrongClickedCubes = getWrongClicks();
const wrongClicksWithinThreshold = wrongClickedCubes.length < numberOfWrongClicksAllowed;
return allCorrectCubesClicked && wrongClicksWithinThreshold;
}

function getWrongClicks() {
return allCubes.filter((item) => {
return item.isClicked && !item.isCorrectAnswer;
});
}

function guessAnswer(guessedCube) {
if(!gameEnded) {
const cubeIndexInArray = allCubes.findIndex((item) => item.cubeIndex === guessedCube.cubeIndex);

let updatedCube = guessedCube;
updatedCube.isClicked = true;

allCubes[cubeIndexInArray] = updatedCube;

const wrongClickedCubes = getWrongClicks();

// if wrong clicks are done, end the game
if(wrongClickedCubes.length >= numberOfWrongClicksAllowed) {
clearInterval(counter);
setTimeout(() => {
hackSuccess = false;
gameTimeRemaining = 0;
gameEnded = true;
return;
}, 500);
}
hackSuccess = isSuccessful();

if(hackSuccess) {
clearInterval(counter);
gameTimeRemaining = 0;
gameEnded = true;
}
}
}

$: {
if(gameEnded) {
if(!isDevMode) {
fetchNui('minigame:callback', hackSuccess);
dispatch('game-ended', { hackSuccess });
}
dispatch('closeUI', {hackSuccess})
}
}

function handleKeyEvent(event) {
let key_pressed = event.key;
let valid_keys = ['Escape'];

if(gameStarted && valid_keys.includes(key_pressed) && !gameEnded) {
switch(key_pressed){
case 'Escape':
closeGame(false);
return;
}
}
}
</script>

<svelte:window on:keydown|preventDefault={handleKeyEvent} />
<div class="memory-game-base">
<div class="time-left">
<i class="fa-solid fa-clock ps-text-lightgrey clock-icon"></i>
<p class="{gameTimeRemaining !== 0 ? 'game-timer-var' : 'mr-1'}">{gameTimeRemaining} </p> time remaining
</div>
<div id="memory-game-container" class="memory-game-container" style="gap: 13px;">
{#each allCubes as cube}
<div
id={'each-cube-'+cube.cubeIndex}
on:click={() => guessAnswer(cube)}
style="width: 0px; height: 0px; border: 0px;"
class="each-cube {gameStarted ? 'cursor-pointer' : 'cursor-default'} {!gameStarted ? (cube.isCorrectAnswer ? 'ps-bg-green-cube' : '') : (cube.isClicked && cube.isCorrectAnswer ? 'ps-bg-green-cube' : (cube.isClicked && !cube.isCorrectAnswer ? 'ps-bg-wrong-cube' : ''))}">
</div>
{/each}
</div>
</div>

<style>
.memory-game-base {
display: flex;
flex-direction: column;

height: 30vw;

justify-content: center;
align-items: center;
color: var(--color-lightgrey);
}

.memory-game-base > .time-left {
display: flex;
flex-direction: row;
justify-content: center;
font-size: 0.85vw;
}
.memory-game-base > .time-left > .clock-icon {
padding-top: 0.17vw;
margin-right: 0.3vw;
}
.memory-game-base > .time-left > .game-timer-var {
width: 2.5vw;
}

.memory-game-base > .memory-game-container {
/* border: 0.1px solid red; */
margin-top: 1vw;
width: 30vw;
height: 29vw;

display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
/* gap: 1.5vw; */
}
.memory-game-base > .memory-game-container > .each-cube {
/* width: 4.5vw;
height: 4.5vw; */

background-color: var(--cube-bg-darkgreen);
border: 2px solid var(--color-green);
}

.ps-bg-green-cube {
background-color: var(--color-green) !important;
}
.ps-bg-wrong-cube {
background-color: var(--color-red) !important;
}
</style>

View file

@ -0,0 +1,331 @@
<script lang="ts">
import { closeGame, gameSettings } from "../stores/GameLauncherStore";
import { createEventDispatcher, onMount } from "svelte";
import fetchNui from "../../utils/fetch";
import { getRandomArbitrary, isDevMode } from "../stores/GeneralStores";

const dispatch = createEventDispatcher();

let gameTimeRemaining = 0;

let gameTime = $gameSettings.gameTime * 100;
let numberOfWrongClicksAllowed = $gameSettings.maxAnswersIncorrect;

let counter, gameStarted = false, gameEnded = false;
let numberOfCubes = 49, allCubes = [];
let blinkingIndex, correctRoute = [], goodPositions = [], stopBlinking = false;
let lastPos = 0, wrongAnswerCount = 0;
let displayCubeNumbers = false;

onMount(() => {
//get starting blinking index
blinkingIndex = getRandomArbitrary(1,4); // 4 => highest index, => lowest index for blink

//generate the correct route
correctRoute = generateBestRoute(blinkingIndex);
goodPositions = Object.keys(correctRoute);

//generating an array to maintain each cube data by index
for(let i = 0; i < numberOfCubes; i++) {
const cubeValue = [blinkingIndex, blinkingIndex * 7].includes(i) ? getRandomArbitrary(1,4) : getRandomArbitrary(1,5);
const cubeData = {
cubeIndex: i,
cubeValue: goodPositions.includes(i.toLocaleString()) ? correctRoute[i] : cubeValue,
classList: ''
};
allCubes.push(cubeData);
allCubes = allCubes;
}

//stop showing the correct cubes and start the guessing game
setTimeout(() => {
gameStarted = true;
counter = setInterval(startTimer, 10);
}, 1000);

});

function maxVertical(pos) {
return Math.floor((48-pos)/7);
}

function maxHorizontal(pos) {
let max = (pos+1) % 7;
if(max > 0) return 7-max;
else return 0;
}

function generateNextPosition(pos) {
let maxV = maxVertical(pos);
let maxH = maxHorizontal(pos);
if(maxV === 0 ){
let new_pos = getRandomArbitrary(getRandomArbitrary(1,maxH), maxH);
return [new_pos, pos+new_pos];
}
if(maxH === 0 ){
let new_pos = getRandomArbitrary(getRandomArbitrary(1,maxV), maxV);
return [new_pos, pos+(new_pos*7)];
}
if(Math.floor(Math.random() * 1000 + 1) % 2 === 0 ){
let new_pos = getRandomArbitrary(getRandomArbitrary(1,maxH), maxH);
return [new_pos, pos+new_pos];
} else {
let new_pos = getRandomArbitrary(getRandomArbitrary(1,maxV), maxV);
return [new_pos, pos+(new_pos*7)];
}
}

function generateBestRoute(start_pos) {
let route = [];
if(getRandomArbitrary(1,1000) % 2 === 0 ){
start_pos *= 7;
}
while(start_pos < 48){
let new_pos = generateNextPosition(start_pos);
route[start_pos] = new_pos[0];
start_pos = new_pos[1];
}
return route;
}

function startTimer() {
if (gameTime <= 0)
{
wrongAnswerCount = numberOfWrongClicksAllowed;
checkMazeAnswer();
return;
}
gameTime--;
gameTimeRemaining = gameTime/100;
}

function updateAllCubesArrayWithClassListOfClickedCube(isGood, clickedCube) {
const additionClassString = isGood ? ' ps-bg-green-cube' : ' ps-bg-wrong-cube';
const newClassList = clickedCube.classList + additionClassString;
clickedCube.classList = newClassList;

//replace the clicked cube data in allCubes array
allCubes[clickedCube.cubeIndex] = clickedCube;
allCubes = allCubes;
}

function handleCubeClick(clickedCube) {
if(!gameEnded && clickedCube.cubeIndex !== 0) {
let posClicked = clickedCube.cubeIndex;
//game just started and user made first click
if(lastPos === 0) {
//stop the blinking and hide numbers on cubes
stopBlinking = true;
if([blinkingIndex, blinkingIndex * 7].includes(posClicked)) {
lastPos = posClicked;

//display good cube click and update allCubes array
updateAllCubesArrayWithClassListOfClickedCube(true, clickedCube);
} else {
wrongAnswerCount++;
//display bad cube click and update allCubes array
updateAllCubesArrayWithClassListOfClickedCube(false, clickedCube);
}
} else {
let posJumps = allCubes[lastPos].cubeValue;
let maxV = maxVertical(lastPos);
let maxH = maxHorizontal(lastPos);

if(posJumps <= maxH && posClicked === lastPos + posJumps) {
lastPos = posClicked;
//display good cube click and update allCubes array
updateAllCubesArrayWithClassListOfClickedCube(true, clickedCube);
} else if (posJumps <= maxV && posClicked === lastPos + (posJumps * 7)) {
lastPos = posClicked;
//display good cube click and update allCubes array
updateAllCubesArrayWithClassListOfClickedCube(true, clickedCube);
} else {
wrongAnswerCount++;
//display bad cube click and update allCubes array
updateAllCubesArrayWithClassListOfClickedCube(false, clickedCube);
}
}
}

checkMazeAnswer();
}

function checkMazeAnswer() {
// check if wrong answers exceeded / game ended - both with same condition
// if yes, end the game, clear counter and display correct answers and then stop game
// else, end the game within few seconds
if(wrongAnswerCount === numberOfWrongClicksAllowed) {
clearInterval(counter);
displayCubeNumbers = true;

allCubes = allCubes.map((cube) => {
cube.classList = goodPositions.includes(cube.cubeIndex.toLocaleString()) ? 'ps-bg-green-cube' : '';
return cube;
});
allCubes = allCubes;

setTimeout(() => {
gameEnded = true;
if(!isDevMode) {
fetchNui('minigame:callback', false);
dispatch('game-ended', { hackSuccess: false });
}
dispatch('closeUI', {hackSuccess: false});
}, 3000);

return;
} else if(lastPos === 48){
clearInterval(counter);
displayCubeNumbers = true;

setTimeout(() => {
gameEnded = true;
if(!isDevMode) {
fetchNui('minigame:callback', true);
dispatch('game-ended', { hackSuccess: true });
}
dispatch('closeUI', {hackSuccess: true});
}, 3000);
}
}

function handleKeyEvent(event) {
let key_pressed = event.key;
let valid_keys = ['Escape'];

if(gameStarted && valid_keys.includes(key_pressed) && !gameEnded) {
switch(key_pressed){
case 'Escape':
closeGame(false);
return;
}
}
}
</script>

<svelte:window on:keydown|preventDefault={handleKeyEvent} />
<div class="maze-game-base">
<div class="time-left">
<i class="fa-solid fa-clock ps-text-lightgrey clock-icon"></i>
<p class="{gameTimeRemaining !== 0 ? 'game-timer-var' : 'mr-1'}">{gameTimeRemaining} </p> time remaining
</div>
<div id="maze-game-container" class="maze-game-container">
{#each allCubes as cube}
<div
id={'each-cube-'+cube.cubeIndex} on:click={() => handleCubeClick(cube)}
class="each-cube {cube.classList}
{[0, numberOfCubes - 1].includes(cube.cubeIndex) ? 'start-dest-cube' : ''}
{!stopBlinking && [blinkingIndex, blinkingIndex * 7].includes(cube.cubeIndex) ? 'blinking-cube' : ''}
"
>
{#if cube.cubeIndex === 0}
<i class="fa-solid fa-ethernet"></i>
{:else if cube.cubeIndex === numberOfCubes - 1}
<i class="fa-solid fa-network-wired"></i>
{:else}
{#if !stopBlinking || displayCubeNumbers}
<p>{ cube.cubeValue }</p>
{/if}
{/if}
</div>
{/each}
</div>
</div>

<style>
.maze-game-base {
display: flex;
flex-direction: column;

height: 28vw;

justify-content: center;
align-items: center;
color: var(--color-lightgrey);
}

.maze-game-base > .time-left {
display: flex;
flex-direction: row;
justify-content: center;
font-size: 0.85vw;
}
.maze-game-base > .time-left > .clock-icon {
padding-top: 0.17vw;
margin-right: 0.3vw;
}
.maze-game-base > .time-left > .game-timer-var {
width: 2.5vw;
}

.maze-game-base > .maze-game-container {
/* border: 0.1px solid red; */
margin-top: 0.5vw;
width: 30vw;
height: 29vw;

display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.9vw;
}
.maze-game-base > .maze-game-container > .start-dest-cube {
background-color: var(--color-green) !important;
color: var(--color-darkblue);

font-size: 1.65vw;
text-align: center;
}
.maze-game-base > .maze-game-container > .start-dest-cube > i {
vertical-align: middle;
}
.maze-game-base > .maze-game-container > .each-cube {
width: 3vw;
height: 3vw;

background-color: var(--cube-bg-darkgreen);
border: 2px solid var(--color-green);
text-align: center;

cursor: default;
}

.maze-game-base > .maze-game-container > .each-cube > p {
font-size: 1.5vw;
font-weight: bold;
margin-top: 0.2vw
}

.blinking-cube {
animation: blink 1s linear infinite;
}

@keyframes blink {
0%,
100% {
background-color: var(--color-green);
/* background-color: var(--cube-bg-darkgreen); */
}

50% {
background-color: var(--cube-bg-darkgreen);
}
}

.ps-bg-green-cube {
background-color: var(--color-green) !important;
}
.ps-bg-wrong-cube {
background-color: var(--color-red) !important;
}

</style>

View file

@ -0,0 +1,240 @@
<script lang="ts">
import { gameSettings, currentActiveGameDetails, closeGame } from "../stores/GameLauncherStore";
import { createEventDispatcher, onMount } from "svelte";
import fetchNui from "../../utils/fetch";
import mojs from '@mojs/core';
import { convertVwToPx, getRandomArbitrary, isDevMode } from "../stores/GeneralStores";

const dispatch = createEventDispatcher();

let gameTimeRemaining = 0;

let blocksInput = $gameSettings.amountOfAnswers; // how many cubes to remember for game - increment number based on difficulty level
let gameTime = $gameSettings.gameTime * 100;
let numberOfWrongClicksAllowed = $gameSettings.maxAnswersIncorrect;
let displayNumbersOnCubesFor = $currentActiveGameDetails.timeForNumberDisplay * 100;

let counter, gameStarted = false, gameEnded = false;
let allCubes = [];
let order = 0, wrongClicks = 0;
let cubeBgColors = ['var(--color-green)', 'var(--color-palegreen)', 'var(--color-blue)'];

// let topLowerBound = 290, topHigherBound = 660; px
// let leftLowerBound = 77, leftHigherBound = 459;
let topLowerBound = 18, topHigherBound = 40; //vw
let leftLowerBound = 2, leftHigherBound = 28;

onMount(() => {
//generate shuffled cubes indices
let cubeIndicesList = [];
while(cubeIndicesList.length < blocksInput){
const r = Math.floor(Math.random() * blocksInput);
if(cubeIndicesList.indexOf(r) === -1) cubeIndicesList.push(r);
}

//generating an array to maintain each cube data by index
for(let i = 0; i < cubeIndicesList.length; i++) {
//height between 290 and 660 px
//horizontal between 77 and 459 px
const cubeData = {
cubeIndex: cubeIndicesList[i],
cubeValue: cubeIndicesList[i],
bgColor: cubeBgColors[Math.floor(Math.random() * cubeBgColors.length)],
top: getRandomArbitrary(topLowerBound, topHigherBound),
left: getRandomArbitrary(leftLowerBound, leftHigherBound)
};
allCubes.push(cubeData);
allCubes = allCubes;
}

//stop showing the correct cubes and start the guessing game
setTimeout(() => {
gameStarted = true;

let eachCube = document.querySelectorAll('.each-cube');
eachCube.forEach(el => { newPos(el) });

counter = setInterval(startTimer, 10);
}, 1000);
});

function newPos(element) {
let top = element.offsetTop;
let left = element.offsetLeft;

let new_top_vw = getRandomArbitrary(topLowerBound,topHigherBound);
let new_left_vw = getRandomArbitrary(leftLowerBound,leftHigherBound);

let new_top = convertVwToPx(new_top_vw);
let new_left = convertVwToPx(new_left_vw);

let diff_top = new_top - top;
let diff_left = new_left - left;
let duration = getRandomArbitrary(10,40)*100;
new mojs.Html({
el: '#'+element.id,
x: {
0:diff_left,
duration: duration,
easing: 'linear.none'
},
y: {
0:diff_top,
duration: duration,
easing: 'linear.none'
},
duration: duration+50,
onComplete() {
if(element.offsetTop === 0 && element.offsetLeft === 0) {
this.pause();
return;
}
const bgColor = element.style.backgroundColor;
element.style = 'background-color: '+bgColor+'; top: '+new_top_vw+'vw; left: '+new_left_vw+'vw; transform: none;';
newPos(element);
},
onUpdate() {
if(gameStarted === false) this.pause();
}
}).play();
}

function startTimer() {
if (gameTime <= 0)
{
endGame(false);
return;
}
displayNumbersOnCubesFor--;
gameTime--;
gameTimeRemaining = gameTime/100;
}

function handleClick(clickedCube) {
if(gameStarted && !gameEnded && displayNumbersOnCubesFor <= 0) {
//correct click
if(order === clickedCube.cubeIndex) {
let clickedCubeDom = document.getElementById('each-cube-'+clickedCube.cubeIndex);
clickedCubeDom.style.backgroundColor = 'var(--color-darkgrey)';
order = order + 1;
} else {
wrongClicks = wrongClicks + 1;
}
checkGameStatus();
}
}

function checkGameStatus() {
if(order === allCubes.length - 1 && wrongClicks < numberOfWrongClicksAllowed) {
endGame(true);
} else if(order < allCubes.length - 1 && wrongClicks >= numberOfWrongClicksAllowed) {
endGame(false);
}
}

function endGame(isSuccess) {
if(!gameEnded) {
gameEnded = true;
clearInterval(counter);
setTimeout(() => {
if(!isDevMode) {
fetchNui('minigame:callback', isSuccess);
dispatch('game-ended', { hackSuccess: isSuccess });
}
dispatch('minigame:callback', {hackSuccess: isSuccess});
}, 1000);
}
}

function handleKeyEvent(event) {
let key_pressed = event.key;
let valid_keys = ['Escape'];

if(gameStarted && valid_keys.includes(key_pressed) && !gameEnded) {
switch(key_pressed){
case 'Escape':
closeGame(false);
return;
}
}
}
</script>

<svelte:window on:keydown|preventDefault={handleKeyEvent} />
<div class="var-game-base">
<div class="time-left">
<i class="fa-solid fa-clock ps-text-lightgrey clock-icon"></i>
<p class="{gameTimeRemaining !== 0 ? 'game-timer-var' : 'mr-1'}">{gameTimeRemaining} </p> time remaining
</div>
<div id="var-game-container" class="var-game-container">
{#each allCubes as cube}
<div
id={'each-cube-'+cube.cubeIndex}
class="each-cube"
style="background-color:{cube.bgColor}; top: {cube.top}vw; left: {cube.left}vw;"
on:click={() => handleClick(cube)}
>
{#if displayNumbersOnCubesFor > 0}
<p>{cube.cubeValue + 1}</p>
{/if}
</div>
{/each}
</div>
</div>

<style>
.var-game-base {
display: flex;
flex-direction: column;

height: 28vw;

justify-content: center;
align-items: center;
color: var(--color-lightgrey);
}

.var-game-base > .time-left {
display: flex;
flex-direction: row;
justify-content: center;
font-size: 0.85vw;
}
.var-game-base > .time-left > .clock-icon {
padding-top: 0.17vw;
margin-right: 0.3vw;
}
.var-game-base > .time-left > .game-timer-var {
width: 2.5vw;
}

.var-game-base > .var-game-container {
border: 2px solid var(--color-green);
background-color: var(--cube-bg-darkgreen);
margin-top: 1vw;
width: 30vw;
height: 28vw;
}
.var-game-base > .var-game-container > .each-cube {
width: 3vw;
height: 3vw;

border: 2px solid var(--color-lightgrey);
position: absolute;

text-align: center;
cursor: default;
}
.var-game-base > .var-game-container > .each-cube > p {
font-size: 1.5vw;
font-weight: bold;
margin-top: 0.2vw;
color: var(--color-black);
}
</style>

View file

@ -0,0 +1,321 @@
<script lang="ts">
import { closeGame, currentActiveGameDetails, gameSettings } from "../stores/GameLauncherStore";
import { createEventDispatcher, onMount } from "svelte";
import fetchNui from "../../utils/fetch";
import { getRandomArbitrary, isDevMode } from "../stores/GeneralStores";

const dispatch = createEventDispatcher();

const randomSetChar = () => {
let str='?';
switch($currentActiveGameDetails.sets) {
case 'numeric':
str="0123456789";
break;
case 'alphabet':
str="ABCDEFGHIJKLMNOPQRSTUVWXYZ";
break;
case 'alphanumeric':
str="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
break;
case 'greek':
str="ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ";
break;
case 'braille':
str="⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿"+
"⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿"+
"⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿";
break;
case 'runes':
str="ᚠᚥᚧᚨᚩᚬᚭᚻᛐᛑᛒᛓᛔᛕᛖᛗᛘᛙᛚᛛᛜᛝᛞᛟᛤ";
break;
}
return str.charAt(getRandomArbitrary(0, str.length));
}

let gameTimeRemaining = 0;

let amountOfAnswers = $gameSettings.amountOfAnswers; // how many numbers for game
let gameTime = $gameSettings.gameTime * 100; // seconds

let correctIndices = [], correctAnswers = [];
let changeBoardAfter = $currentActiveGameDetails.changeBoardAfter * 100; //3 seconds
let originalChangeBoardAfter = changeBoardAfter;
let counter, gameStarted = false, gameEnded = false;
let hackSuccess = false;
let numberOfCubes = 80, allCubes = [];
let totalNumberOfColumns = 10;

let cursorIndices = [], cursorStartIndex = 43;

onMount(() => {
//generating an array to maintain each cube data by index
for(let i = 0; i < numberOfCubes; i++) {
const cubeData = {
cubeIndex: i,
cubeValue: randomSetChar() + randomSetChar(),
};
allCubes.push(cubeData);
allCubes = allCubes;
}

//generate correct answer values - column number - has to be Number of cols - 4 to have straight 4 answers
const columnNumber = Math.floor(Math.random() * 5);
//generate correct answer values - row number
const rowNumber = Math.floor(Math.random() * 7);

const startIndex = rowNumber * totalNumberOfColumns + columnNumber;
correctAnswers = [];
for(let i = 0; i < amountOfAnswers; i++) {
correctAnswers.push(allCubes[i + startIndex]);
correctIndices.push(i+startIndex);
}

getCursorIndices();

//start game
setTimeout(() => {
gameStarted = true;
counter = setInterval(startTimer, 10);
}, 1000);

});

function getCursorIndices() {
cursorIndices = [cursorStartIndex];
for(let i=1; i<4; i++){
if( cursorStartIndex+i >= 80 ){
cursorIndices.push( (cursorStartIndex+i) - 80 );
}else{
cursorIndices.push( cursorStartIndex+i );
}
}
// return group;
}

function endTheGame() {
clearInterval(counter);
gameEnded = true;

setTimeout(() => {
if(!isDevMode) {
fetchNui('minigame:callback', hackSuccess);
dispatch('game-ended', { hackSuccess });
}
dispatch('minigame:callback', hackSuccess);
}, 500);
}

function startTimer() {
if (gameTime <= 0)
{
hackSuccess = false;
endTheGame();
return;
} else if (changeBoardAfter <= 0) {
scrambleBoard();
}

gameTime--;
changeBoardAfter--;
gameTimeRemaining = gameTime/100;
}

function scrambleBoard() {
changeBoardAfter = originalChangeBoardAfter;

//generating an array to maintain each cube data by index
let newCubeData = [];
for(let i = 0; i < numberOfCubes; i++) {
let cubeValue;

if(i === numberOfCubes - 1) {
cubeValue = allCubes[0].cubeValue;
} else {
cubeValue = allCubes[i+1].cubeValue;
}
const cubeData = {
cubeIndex: i,
cubeValue: cubeValue,
};
newCubeData.push(cubeData);
newCubeData = newCubeData;
}
getCursorIndices();

allCubes = newCubeData;
}

function checkAnswer() {
let selectedValues = cursorIndices.map((currentCursorIndex) => {
return allCubes[currentCursorIndex];
});
const selectedValuesData = selectedValues.map((item) => {
return item.cubeValue;
});

const correctAnswerValues = correctAnswers.map((item) => {
return item.cubeValue;
});

if(JSON.stringify(selectedValuesData) === JSON.stringify(correctAnswerValues)) {
hackSuccess = true;
} else {
hackSuccess = false;
}

endTheGame();
}

function handleKeyEvent(event) {
let key_pressed = event.key;
let valid_keys = ['a','w','s','d', 'A','W','S','D' ,'ArrowUp','ArrowDown','ArrowRight','ArrowLeft','Enter', 'Escape'];

if(gameStarted && valid_keys.includes(key_pressed) && !gameEnded) {
switch(key_pressed){
case 'w':
case 'ArrowUp':
cursorStartIndex -= 10;
if(cursorStartIndex < 0) {
cursorStartIndex += 80;
}
break;
case 's':
case 'ArrowDown':
cursorStartIndex += 10;
cursorStartIndex %= 80;
break;
case 'a':
case 'ArrowLeft':
cursorStartIndex--;
if(cursorStartIndex < 0) {
cursorStartIndex = 79;
}
break;
case 'd':
case 'ArrowRight':
cursorStartIndex++;
cursorStartIndex %= 80;
break;
case 'Enter':
clearInterval(counter);
checkAnswer();
return;
case 'Escape':
closeGame(false);
return;
}
}
}

$: {
if(cursorStartIndex) {
getCursorIndices();
}
}
</script>

<svelte:window on:keydown|preventDefault={handleKeyEvent} />
<div class="scrambler-game-base">
<div class="game-info-container">
<div class="scrambler-find-data">
<p>Match the numbers underneath.</p>
<div class="original-data-wrapper">
{#each correctAnswers as value}
<p class="original-digits">{value.cubeValue}</p>
{/each}
</div>
</div>
<div class="time-left">
<i class="fa-solid fa-clock ps-text-lightgrey clock-icon"></i>
<p class="{gameTimeRemaining !== 0 ? 'game-timer-var' : 'mr-1'}">{gameTimeRemaining} </p> time remaining
</div>
</div>
<div id="scrambler-game-container" class="scrambler-game-container">
{#each allCubes as cube}
<div id={'each-cube-'+cube.cubeIndex} class="each-cube">
<p class="{!gameEnded && cursorIndices.includes(cube.cubeIndex) ? 'ps-text-red' : ''}">{cube.cubeValue}</p>
</div>
{/each}
</div>
</div>

<style>
.scrambler-game-base {
display: flex;
flex-direction: column;
height: 28vw;

justify-content: center;
align-items: center;
color: var(--color-lightgrey);
}

.scrambler-game-base > .game-info-container {
display: flex;
flex-direction: column;
justify-content: center;
}

.scrambler-game-base > .game-info-container > .scrambler-find-data {
display: flex;
flex-direction: column;
}

.scrambler-game-base > .game-info-container > .scrambler-find-data > .original-data-wrapper {
width: max-content;
border: 2px solid var(--color-green);
border-radius: 0.2vw;
background-color: var(--cube-bg-darkgreen);
color: var(--color-green);

margin: 0.5vw auto;

display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.scrambler-game-base > .game-info-container > .scrambler-find-data > .original-data-wrapper > .original-digits {
font-size: 1vw;
padding: 0.3vw 0.5vw;
}

.scrambler-game-base > .game-info-container > .time-left {
display: flex;
flex-direction: row;
justify-content: center;
font-size: 0.85vw;
}
.scrambler-game-base > .game-info-container > .time-left > .clock-icon {
padding-top: 0.17vw;
margin-right: 0.3vw;
}
.scrambler-game-base > .game-info-container > .time-left > .game-timer-var {
width: 2.5vw;
}

.scrambler-game-base > .scrambler-game-container {
/* border: 0.1px solid red; */
margin-top: 1vw;
width: 30vw;
height: 24vw;

display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5vw;
}
.scrambler-game-base > .scrambler-game-container > .each-cube {
width: 2.5vw;
height: 2.5vw;

font-size: 1.75vw;
text-align: center;
margin: auto;
}
</style>

View file

@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from "svelte";
import Skull from "../assets/svgs/Skull.svelte";
import { closeUi } from "../stores/GameLauncherStore";

export let isSuccess = false;
const skullColor: string = 'var(--color-green)';

onMount(() => {
setTimeout(() => {
closeUi();
}, 2000);
})
</script>

<div class="result-container">
<div class="result-wrapper ps-bg-darkblue">
<div class="skull-logo">
<Skull color={skullColor} />
</div>
{#if isSuccess}
<h1 class="text-white text-3xl uppercase">
Access granted
</h1>
{:else}
<h1 class="text-white text-3xl uppercase">
Access denied
</h1>
{/if}
</div>
</div>

<style>
.result-container {
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
border-radius: 0.2vw;
}

.result-container > .result-wrapper {
width: 30vw;
height: 15vw;
border-radius: 0.3vw;
overflow: hidden;

display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.result-container > .result-wrapper > .skull-logo {
margin: 0 auto 1vw auto;
width: 6vw;
}
</style>

View file

@ -0,0 +1,86 @@
@tailwind base;
@tailwind components;
@tailwind utilities;



@font-face {
font-family: roboto;
src: url('./assets/fonts/Roboto/Roboto-Regular.ttf');
}

@font-face {
font-family: hacker;
src: url('./assets/fonts/Hacker-Technology-Font/Hacker.ttf');
}

@font-face {
font-family: arcade;
src: url('./assets/fonts/karmatic_arcade/ka1.ttf');
}

@layer utilities {
.ps-text-roboto {
font-family: 'roboto';
}
.ps-font-arcade {
font-family: 'arcade';
}
.ps-font-hacker {
font-family: 'hacker';
}

.ps-text-green {
color: var(--color-green) !important;
}
.ps-text-lightgrey {
color: var(--color-lightgrey);
}
.ps-text-darkblue {
color: var(--color-darkblue);
}
.ps-text-red {
color: var(--color-red) !important;
}

.ps-border-green {
border: 1px solid var(--color-green);
}
.ps-outline-green {
outline: 1px solid var(--color-green);
}

.ps-bg-lightgrey {
background-color: var(--color-lightgrey);
}
.ps-bg-darkblue {
background-color: var(--color-darkblue);
}
.ps-bg-green {
background-color: var(--color-green);
}

.ps-bg-green-w-opacity {
background-color: rgba(2, 241, 181, 0.2);
}
.ps-notification-success {
background-color: #2ebd2e;
color: black;
border: 1px solid #158515;
}
.ps-notification-warning {
background-color: #fbb433;
border: 1px solid #cf8d15;
color: black;
}
.ps-notification-error {
background-color: #cd2222;
border: 1px solid #8d0e0e;
color: white;
}
.ps-notification-info {
background-color: #33addf;
border: 1px solid #1886b3;
color: black;
}
}

View file

@ -0,0 +1,6 @@
export interface IDrawText {
// title: string;
icon: string;
keys: string;
color: string;
}

View file

@ -0,0 +1,8 @@
export interface IGameSettings {
game: string;
gameName: string;
gameDescription: string;
gameTime: number;
amountOfAnswers: number;
maxAnswersIncorrect: number;
}

View file

@ -0,0 +1,5 @@
export interface IImage {
action?: string;
show: boolean;
url: string;
}

View file

@ -0,0 +1,7 @@
export interface IInput {
id: string;
label: string;
icon: string;
placeholder?: string;
type: string;
}

View file

@ -0,0 +1,15 @@
export interface IMenu {
header: string;
text?: string;
icon: string;
color: string;
callback: string;
subMenu: Array<null|ISubMenu>;
};

export interface ISubMenu {
header: string;
text?: string;
icon: string;
color: string;
};

View file

@ -0,0 +1,8 @@
import type { NotificationTypes } from './../enums/NotificationTypesEnum';

export interface INotification {
text: string;
type: NotificationTypes;
icon: string;
length: number;
}

View file

@ -0,0 +1,11 @@
export interface IStatusBar {
title: string;
description: string;
icon: string;
items: Array<IStatusBarItem>;
}

export interface IStatusBarItem {
key: string;
value: string;
}

View file

@ -0,0 +1,9 @@
import App from './App.svelte'
import './index.css'
import '@fortawesome/fontawesome-free/css/all.css'

const app = new App({
target: document.getElementById('app')
})

export default app

View file

@ -0,0 +1,25 @@
import { writable } from 'svelte/store';
import CircleGame from '../games/CircleGame.svelte';

const circleGameStore = writable<CircleGame | null>(null);

export function initializeCircleGame() {
circleGameStore.update(component => {
if (!component) {
const newComponent = new CircleGame({
target: document.body
});
return newComponent;
}
return component;
});
}

export function setupCircleGame(data: { circles?: number; time?: number }) {
circleGameStore.update(component => {
if (component) {
component.setupCircleGame(data);
}
return component;
});
}

View file

@ -0,0 +1,29 @@
import { writable, type Writable } from 'svelte/store';
import { showComponent, showUi } from './GeneralStores';
import { UIComponentsEnum } from '../enums/UIComponentsEnum';
import type { IDrawText } from '../interfaces/IDrawText';

export const drawTextStore: Writable<IDrawText> = writable({
// title: '',
icon: '',
keys: '',
color: ''
});

export const hideDrawTextStore: Writable<any> = writable(false);

export function showDrawTextMenu(data: IDrawText) {
showUi.set(true);
showComponent.set(UIComponentsEnum.DrawText);
drawTextStore.set({
// title: data.title,
icon: data.icon || 'fa-solid fa-circle-info',
keys: data.keys,
color: data.color || 'var(--color-green)'
});
}

export function hideDrawTextMenu() {
hideDrawTextStore.set(true);
}

View file

@ -0,0 +1,118 @@
import { UIComponentsEnum } from '../enums/UIComponentsEnum';
import { GamesEnum } from '../enums/GamesEnum';
import { ConnectingGameMessageEnum } from '../enums/GameConnectionMessages';
import { hideUi, isDevMode, showComponent } from './GeneralStores';
import type { IGameSettings } from './../interfaces/IGameSettings';
import { writable, type Writable } from 'svelte/store';
import fetchNui from './../../utils/fetch';

export const gameSettings: Writable<IGameSettings> = writable({
game: '',
gameName: '',
gameDescription: '',
amountOfAnswers: 0,
gameTime: 0,
maxAnswersIncorrect: 0,
});
export const currentGameActive: Writable<GamesEnum> | undefined = writable();
export const currentActiveGameDetails: Writable<any> | undefined = writable();
export const connectionText: Writable<ConnectingGameMessageEnum> = writable();
export const showLoading: Writable<boolean> = writable(true);

export function setupGame(data): void {
const settings = data;
currentActiveGameDetails.set(settings);

switch (settings.game) {
case GamesEnum.Memory: {
showComponent.set(UIComponentsEnum.Game);
currentGameActive.set(GamesEnum.Memory);
connectionText.set(ConnectingGameMessageEnum.Connecting);

gameSettings.set({
game: GamesEnum.Memory,
gameName: settings.gameName,
gameDescription: settings.gameDescription,
gameTime: settings.gameTime || 2,// 1000 = 10 seconds
amountOfAnswers: settings.amountOfAnswers || 15,
maxAnswersIncorrect: settings.maxAnswersIncorrect || 2
});

break;
}

case GamesEnum.Scrambler: {
showComponent.set(UIComponentsEnum.Game);
currentGameActive.set(GamesEnum.Scrambler);
connectionText.set(ConnectingGameMessageEnum.Connecting);

gameSettings.set({
game: GamesEnum.Scrambler,
gameName: settings.gameName,
gameDescription: settings.gameDescription,
gameTime: settings.gameTime || 2,// 1000 = 10 seconds
amountOfAnswers: settings.amountOfAnswers || 4,
maxAnswersIncorrect: settings.maxAnswersIncorrect || 0,
});

break;
}

case GamesEnum.NumberMaze: {
showComponent.set(UIComponentsEnum.Game);
currentGameActive.set(GamesEnum.NumberMaze);
connectionText.set(ConnectingGameMessageEnum.Connecting);

gameSettings.set({
game: GamesEnum.NumberMaze,
gameName: settings.gameName,
gameDescription: settings.gameDescription,
gameTime: settings.gameTime || 2,// 1000 = 10 seconds
amountOfAnswers: settings.amountOfAnswers || 4,
maxAnswersIncorrect: settings.maxAnswersIncorrect || 0,
});
break;
}

case GamesEnum.NumberPuzzle: {
showComponent.set(UIComponentsEnum.Game);
currentGameActive.set(GamesEnum.NumberPuzzle);
connectionText.set(ConnectingGameMessageEnum.Connecting);

gameSettings.set({
game: GamesEnum.NumberPuzzle,
gameName: settings.gameName,
gameDescription: settings.gameDescription,
gameTime: settings.gameTime || 2,// 1000 = 10 seconds
amountOfAnswers: settings.amountOfAnswers || 4,
maxAnswersIncorrect: settings.maxAnswersIncorrect || 0,
});
break;
}
}
}

export function closeGame(isSuccess: boolean): void {
if(!isDevMode) {
fetchNui('minigame:callback', isSuccess);
}
closeUi();
}

export function closeUi() {
hideUi();
currentGameActive.set(null);
currentActiveGameDetails.set(null);
gameSettings.set({
game: '',
gameName: '',
gameDescription: '',
amountOfAnswers: 0,
gameTime: 0,
maxAnswersIncorrect: 0,
});
}

View file

@ -0,0 +1,22 @@
import type { UIComponentsEnum } from './../enums/UIComponentsEnum';
import { writable, type Writable } from 'svelte/store';
import { currentGameActive } from './GameLauncherStore';

export const showComponent: Writable<UIComponentsEnum | string> = writable();

export const showUi: Writable<boolean> = writable();
export const isDevMode = false;

export function hideUi(): void {
showUi.set(false);
showComponent.set(undefined);
currentGameActive.set(undefined);
}

export function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}

export function convertVwToPx(vw) {
return (document.documentElement.clientWidth * vw) / 100;
}

View file

@ -0,0 +1,15 @@
import type { IImage } from './../interfaces/IImage';
import { writable, type Writable } from 'svelte/store';
import { showComponent, showUi } from './GeneralStores';
import { UIComponentsEnum } from './../enums/UIComponentsEnum';

export const imageStore: Writable<IImage> = writable({ show: false, url: '' });

export function showImage(event: any) {
showUi.set(true);
showComponent.set(UIComponentsEnum.Image);
imageStore.set({
show: event.show,
url: event.url,
});
}

View file

@ -0,0 +1,36 @@
import { writable, type Writable } from 'svelte/store';
import type { IInput } from './../interfaces/IInput';
import { showComponent, showUi } from './GeneralStores';
import { UIComponentsEnum } from '../enums/UIComponentsEnum';

export const inputStore: Writable<Array<IInput>> = writable([
{
id: '1',
label: 'Label',
icon: 'fa-solid fa-user',
placeholder: 'Insert name',
type: 'text',
},
{
id: '2',
label: 'Label',
icon: 'fa-solid fa-user',
placeholder: 'Placeholder',
type: 'password',
},
{
id: '3',
label: 'Label',
icon: 'fa-solid fa-user',
placeholder: 'Placeholder',
type: 'phone',
},
]);

/** Show the input component */
export function showInput(data: Array<IInput>): void {
showUi.set(true);
showComponent.set(UIComponentsEnum.Input);

inputStore.set([...data]);
}

View file

@ -0,0 +1,32 @@
import type { IMenu } from './../interfaces/IMenu';
import { writable, type Writable } from 'svelte/store';
import { hideUi, showComponent, showUi } from './GeneralStores';
import { UIComponentsEnum } from '..//enums/UIComponentsEnum';
import fetchNui from '../../utils/fetch';

export const menuStore: Writable<Array<IMenu>> = writable([]);

export const menuIdShown: Writable<string> = writable();

export function setupInteractionMenu(data: Array<IMenu>): void {
showUi.set(true);
showComponent.set(UIComponentsEnum.Menu);
menuStore.set(data.menuData);
}

export function closeInteractionMenu(): void {
showUi.set(false);
showComponent.set(null);
menuStore.set([
{
header: '',
text: '',
icon: '',
color: '',
callback: '',
subMenu: null,
}
]);
fetchNui('menuClose');
hideUi();
}

View file

@ -0,0 +1,30 @@
import type { INotification } from 'src/interfaces/INotification';
import { writable, type Writable } from 'svelte/store';
import { showComponent } from './GeneralStores';
import { UIComponentsEnum } from './../enums/UIComponentsEnum';

export const notifications: Writable<Array<INotification>> = writable([]);

export function addNotification(newNotification: INotification): void {
showComponent.set(UIComponentsEnum.Notification);
notifications.update((currentNotifications) => {
const updatedNotifications = [...currentNotifications, newNotification];
return updatedNotifications;
});

const unsubscribe = notifications.subscribe((data: Array<INotification>) => {
data.forEach((notification: INotification) => {
setTimeout(() => {
removeNotification(notification);
}, notification.length);
});
});
unsubscribe();
}

function removeNotification(notification: INotification): void {
notifications.update((currentNotifications) => {
const updatedNotifications = currentNotifications.filter((n) => n !== notification);
return updatedNotifications;
});
}

View file

@ -0,0 +1,29 @@
import { writable, type Writable } from 'svelte/store';
import type { IStatusBar } from '../interfaces/IStatusBar';
import { showComponent, showUi } from './GeneralStores';
import { UIComponentsEnum } from '../enums/UIComponentsEnum';

export const statusBarStore: Writable<IStatusBar> = writable({
title: '',
description: '',
items: [],
icon: '',
});

export const hideStatusBarStore: Writable<any> = writable(false);

export function showStatusBar(data: IStatusBar) {
showUi.set(true);
showComponent.set(UIComponentsEnum.StatusBar);
statusBarStore.set({
title: data.title,
description: data.description,
items: data.items,
icon: data.icon || 'fa-solid fa-circle-info',
});
}

export function hideStatusBar() {
hideStatusBarStore.set(true);
}

View file

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />