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,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,48 @@
# Svelte + TS + Vite

This template should help get you started developing with Svelte and TypeScript in Vite.

## Recommended IDE Setup

[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).

## Need an official Svelte framework?

Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.

## Technical considerations

**Why use this over SvelteKit?**

- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.

This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.

Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.

**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**

Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.

**Why include `.vscode/extensions.json`?**

Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.

**Why enable `allowJs` in the TS template?**

While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.

**Why is HMR not preserving my local component state?**

HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).

If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.

```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1 @@
@import './themes.css';

View file

@ -0,0 +1,16 @@
:root {
--color-green: #02f1b5;
--color-palegreen: #039672;
--color-darkblue: #131313;
--color-blue: #0057ae;
--color-lightgrey: #dadada;
--color-white: #ffffff;
--color-black: #000000;
--color-darkgrey: #6a6a6a;
--color-red: #e10404;

--cube-bg-darkgreen: #0f2a25;

font-size: 0.8em;
font-family: 'roboto', sans-serif;
}

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./public/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<title>PS-UI</title>
<link rel="stylesheet" href="./css/master.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
{
"name": "ps-ui",
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tsconfig/svelte": "^5.0.2",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.33",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"svelte-preprocess": "^5.1.3",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"html-minifier": "^4.0.0"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@mojs/core": "^1.7.1"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
import tailwind from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import tailwindConfig from './tailwind.config.cjs';

export default {
plugins: [tailwind(tailwindConfig), autoprefixer],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

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

View file

@ -0,0 +1,7 @@
import sveltePreprocess from 'svelte-preprocess'

export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess()
}

View file

@ -0,0 +1,11 @@
module.exports = {
darkmode: true,
content: [
"./index.html",
"./src/**/*.{svelte,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View file

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"resolveJsonModule": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

View file

@ -0,0 +1,105 @@
import { hideStatusBar, showStatusBar } from '../src/stores/StatusBarStores';
import { hideUi, showComponent, showUi } from '../src/stores/GeneralStores';
import { showInput } from './../src/stores/InputStores';

import { onMount, onDestroy } from 'svelte';
import fetchNui from './fetch';
import { UIComponentsEnum } from './../src/enums/UIComponentsEnum';
import { currentActiveGameDetails, currentGameActive, gameSettings, setupGame } from '../src/stores/GameLauncherStore';
import { showImage } from './../src/stores/ImageStore';
import { addNotification } from './../src/stores/NotificationStore';
import { hideDrawTextMenu, showDrawTextMenu } from '../src/stores/DrawTextStore';
import { closeInteractionMenu, setupInteractionMenu } from '../src/stores/MenuStores';
import { initializeCircleGame, setupCircleGame } from '../src/stores/CircleGameStore';

interface nuiMessage {
action: string;
data: {[key: string]: any};
}

export function EventHandler() {
function mainEvent(event: nuiMessage) {
showUi.set(true);
switch (event.data.action) {
case 'ShowStatusBar':
showStatusBar(event.data.data as any);
break;
case 'UpdateStatusBar':
showStatusBar(event.data.data as any);
break;
case 'HideStatusBar':
hideStatusBar();
break;
case 'ShowMenu':
setupInteractionMenu(event.data.data as any)
break;
case 'HideMenu':
closeInteractionMenu();
break;
case 'ShowInput':
showInput(event.data.data as any);
break;
case 'ShowImage':
showImage(event.data.data as any);
break;
case 'hideUi':
hideUi();
break;
case 'ShowNotification':
addNotification(event.data.data as any);
break;
case 'ShowDrawTextMenu':
showDrawTextMenu(event.data.data as any);
break;
case 'HideDrawTextMenu':
hideDrawTextMenu();
break;
case 'CircleGame':
initializeCircleGame()
setupCircleGame(event.data.data)
break;
case 'MemoryGame':
case 'Scramber':
case 'NumberMaze':
case 'GameLauncher':
setupGame(event.data.data as any);
break;
}
}

onMount(() => window.addEventListener('message', mainEvent));
onDestroy(() => window.removeEventListener('message', mainEvent));
}

export function handleKeyUp(event: KeyboardEvent) {
const charCode = event.key;
if (charCode == 'Escape') {
showComponent.subscribe((component: UIComponentsEnum) => {
switch (component) {
case UIComponentsEnum.Input:
fetchNui('input-close', { ok: true });
break;
case UIComponentsEnum.Menu:
closeInteractionMenu();
break;
case UIComponentsEnum.Image:
fetchNui('minigame:callback', true);
break;
case UIComponentsEnum.Game:
currentGameActive.set(null);
currentActiveGameDetails.set(null);
gameSettings.set({
game: '',
gameName: '',
gameDescription: '',
amountOfAnswers: 0,
gameTime: 0,
maxAnswersIncorrect: 0,
triggerEvent: '',
});
break;
}
});
hideUi();
}
}

View file

@ -0,0 +1,28 @@
export default async function fetchNui(eventName: string, data: unknown = {}) {
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: JSON.stringify(data),
};

const getResourceName = () => {
try {
// @ts-ignore
return window.GetParentResourceName();
} catch (err) {
return 'ps-ui';
}
};

const resourceName = getResourceName();

try {
const resp = await fetch(
`https://${resourceName}/${eventName}`,
options
);
return await resp.json();
} catch (err) {}
}

View file

@ -0,0 +1,45 @@
import type { IImage } from './../src/interfaces/IImage';
export default function mockEventCall(data: unknown = {}) {
window.dispatchEvent(new MessageEvent('message', { data }));
}

export function newMemoryGameMock() {
setTimeout(() => {
mockEventCall({
action: 'MemoryGame',
data: {
game: 'MemoryGame',
amountOfAnswers: 1,
gameTime: 5,
maxAnswersIncorrect: 2,
triggerEvent: '',
},
});
}, 100);
}

export function showImageMock() {
setTimeout(() => {
mockEventCall({
action: 'ShowImage',
data: {
show: true,
url: 'https://i.ytimg.com/vi/7V15_-32iCU/maxresdefault.jpg',
},
});
}, 100);
}

export function notificationMock() {
setTimeout(() => {
mockEventCall({
action: 'ShowNotification',
data: {
text: 'New notification',
type: 'ps-notification-success',
icon: 'fa-solid fa-times',
length: 5000,
},
});
}, 500);
}

View file

@ -0,0 +1,42 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import postcss from './postcss.config.js';
import { minify } from 'html-minifier';

const minifyHtml = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
return minify(html, {
collapseWhitespace: true,
});
},
};
};

// https://vitejs.dev/config/
export default defineConfig({
css: {
postcss,
},
plugins: [svelte(), minifyHtml()],
test: {
globals: true,
environment: 'jsdom',
},
base: './', // fivem nui needs to have local dir reference
build: {
minify: true,
emptyOutDir: true,
outDir: '../web/build',
assetsDir: './',
rollupOptions: {
output: {
// By not having hashes in the name, you don't have to update the manifest, yay!
entryFileNames: `[name].js`,
chunkFileNames: `[name].js`,
assetFileNames: `[name].[ext]`,
},
},
},
});