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>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M512 208v224c0 8.75-7.25 16-16 16H416v-128h-32v128h-64v-128h-32v128H224v-128H192v128H128v-128H96v128H16C7.25 448 0 440.8 0 432v-224C0 199.2 7.25 192 16 192H64V144C64 135.2 71.25 128 80 128H128V80C128 71.25 135.2 64 144 64h224C376.8 64 384 71.25 384 80V128h48C440.8 128 448 135.2 448 144V192h48C504.8 192 512 199.2 512 208z"/></svg>

After

Width:  |  Height:  |  Size: 570 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M400 0C426.5 0 448 21.49 448 48V144C448 170.5 426.5 192 400 192H352V224H608C625.7 224 640 238.3 640 256C640 273.7 625.7 288 608 288H512V320H560C586.5 320 608 341.5 608 368V464C608 490.5 586.5 512 560 512H400C373.5 512 352 490.5 352 464V368C352 341.5 373.5 320 400 320H448V288H192V320H240C266.5 320 288 341.5 288 368V464C288 490.5 266.5 512 240 512H80C53.49 512 32 490.5 32 464V368C32 341.5 53.49 320 80 320H128V288H32C14.33 288 0 273.7 0 256C0 238.3 14.33 224 32 224H288V192H240C213.5 192 192 170.5 192 144V48C192 21.49 213.5 0 240 0H400zM256 64V128H384V64H256zM224 448V384H96V448H224zM416 384V448H544V384H416z"/></svg>

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" />