From 5ccfe937193c3b8533873d1813c420337709d0c0 Mon Sep 17 00:00:00 2001 From: Nick Kosarev Date: Wed, 3 Jul 2024 11:42:47 +0000 Subject: [PATCH 1/4] chore: some color changes --- .vscode/settings.json | 2 - src/lib/components/Footer.svelte | 4 +- src/lib/components/Hamburger.svelte | 1 - src/lib/components/MenuDesktop.svelte | 6 +- src/lib/styles/styles.css | 40 ++-- .../(game)/play/GamePollProgress.svelte | 2 +- .../[lang]/(game)/play/GameRouteInfo.svelte | 2 +- .../[lang]/(website)/coupon/+page.svelte | 221 ++++++++++-------- .../(website)/p/[userName]/+page.svelte | 2 +- .../[lang]/(website)/trophy/[id]/+page.svelte | 182 ++++++++------- 10 files changed, 245 insertions(+), 217 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3a1d500e..cea90f0a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,4 @@ { - "eslint.experimental.useFlatConfig": true, - "prettier.enable": false, "editor.formatOnSave": false, diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index b890c7cf..6f771141 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -37,11 +37,11 @@ a { font-weight: 600; - color: var(--color-text); + color: inherit; } a:hover { - color: var(--color-accent-1); + color: var(--green-9); } @media (min-width: 480px) { diff --git a/src/lib/components/Hamburger.svelte b/src/lib/components/Hamburger.svelte index 8b82ddfd..7c72c2da 100644 --- a/src/lib/components/Hamburger.svelte +++ b/src/lib/components/Hamburger.svelte @@ -32,7 +32,6 @@ svg { min-height: 24px; transition: transform 0.2s ease-in-out; - color: var(--color-text); } svg line { diff --git a/src/lib/components/MenuDesktop.svelte b/src/lib/components/MenuDesktop.svelte index f1901c4b..34663a81 100644 --- a/src/lib/components/MenuDesktop.svelte +++ b/src/lib/components/MenuDesktop.svelte @@ -45,7 +45,7 @@ display: flex; height: 100%; align-items: center; - color: var(--color-text); + color: inherit; font-weight: 700; font-size: 1rem; text-transform: uppercase; @@ -55,7 +55,7 @@ } nav a:hover { - color: var(--color-accent-1); + color: var(--green-9); } ul { @@ -77,6 +77,6 @@ } li[aria-current='page'] a { - color: var(--color-accent-1); + color: var(--green-9); } diff --git a/src/lib/styles/styles.css b/src/lib/styles/styles.css index 6527c9c1..ebd13d2f 100644 --- a/src/lib/styles/styles.css +++ b/src/lib/styles/styles.css @@ -1,8 +1,11 @@ @import url('https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wdth,wght@0,62.5..100,100..900;1,62.5..100,100..900&display=swap'); +@import '@radix-ui/colors/bronze.css'; +@import '@radix-ui/colors/brown.css'; +@import '@radix-ui/colors/green.css'; +@import '@radix-ui/colors/orange.css'; + :root { - --color-accent-1: #30A46C; - --color-text: #331E0B; --color-twitch: #8E4EC6; --color-background: #FFF7ED; --color-background-2: #F6EEE7; @@ -22,14 +25,15 @@ html { font-size: 16px; } -html, body { +html, +body { font-family: "Noto Serif", "Times New Roman", "Georgia", serif; font-optical-sizing: auto; font-weight: 500; font-style: normal; font-variation-settings: "wdth" 100; - background-color: var(--color-background); - color: var(--color-text); + background-color: var(--orange-2); + color: var(--bronze-12); } body { @@ -60,7 +64,7 @@ p { } a { - color: var(--color-accent-1); + color: var(--green-9); text-decoration: none; } @@ -84,7 +88,6 @@ pre { box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); padding: 0.5em; overflow-x: auto; - color: var(--color-text); } input, @@ -93,7 +96,9 @@ button { font-family: inherit; } -button, input[type="submit"], input[type="reset"] { +button, +input[type="submit"], +input[type="reset"] { background: none; color: inherit; border: none; @@ -139,19 +144,20 @@ button:focus:not(:focus-visible) { bottom: -0.25em; transform: translateY(100%); padding: 0.5em; - background-color: var(--color-background-2); + background-color: var(--brown-3); z-index: 5; - } +} - .dropdown-menu a { +.dropdown-menu a { color: inherit; text-decoration: none; - } +} - .dropdown-menu button { +.dropdown-menu button { text-align: left; - } +} - .dropdown-menu button:hover, .dropdown-menu a:hover { - color: var(--color-accent-1); - } +.dropdown-menu button:hover, +.dropdown-menu a:hover { + color: var(--green-9); +} \ No newline at end of file diff --git a/src/routes/[lang]/(game)/play/GamePollProgress.svelte b/src/routes/[lang]/(game)/play/GamePollProgress.svelte index b3d69785..f7b17952 100644 --- a/src/routes/[lang]/(game)/play/GamePollProgress.svelte +++ b/src/routes/[lang]/(game)/play/GamePollProgress.svelte @@ -65,7 +65,7 @@ width: 100%; height: 100%; display: none; - background-color: var(--color-accent-1); + background-color: var(--green-9); } .votes { diff --git a/src/routes/[lang]/(game)/play/GameRouteInfo.svelte b/src/routes/[lang]/(game)/play/GameRouteInfo.svelte index 97cb90bd..7e7f6368 100644 --- a/src/routes/[lang]/(game)/play/GameRouteInfo.svelte +++ b/src/routes/[lang]/(game)/play/GameRouteInfo.svelte @@ -61,7 +61,7 @@ .progress-bar { z-index: 10; height: 1.5em; - background-color: var(--color-accent-1); + background-color: var(--green-9); border-radius: 1em; } diff --git a/src/routes/[lang]/(website)/coupon/+page.svelte b/src/routes/[lang]/(website)/coupon/+page.svelte index 6ccb8932..a80adc81 100644 --- a/src/routes/[lang]/(website)/coupon/+page.svelte +++ b/src/routes/[lang]/(website)/coupon/+page.svelte @@ -9,7 +9,7 @@ export let data - const latestCoupons = data.latestCoupons.slice(0, 6) + const latestCoupons = data.latestCoupons.slice(0, 12) TimeAgo.addLocale(ru) @@ -23,14 +23,19 @@

Купон

-

Уже встречался на стриме?

+

+ Уже встречался на стриме? +

banana coupon

Последние активации

-

На стриме периодически появляются сообщения с инструкцией, как получить купон.

+

+ На стриме периодически появляются сообщения с инструкцией, как получить + купон. +

{#each latestCoupons as coupon} @@ -42,7 +47,9 @@
- {coupon.profile.userName} + {coupon.profile.userName}
@@ -52,7 +59,9 @@

Куда его можно потратить

-

Важно: действия имеют статус "в разработке". Развлечемся!

+

+ Важно: действия имеют статус "в разработке". Развлечемся! +

@@ -60,7 +69,10 @@

Сеть персонажей

-

Персонажи "прокачиваются" за счет заметок игроков: истории, детали характера, визуальные свойства. Для их публикации нужны купоны.

+

+ Персонажи "прокачиваются" за счет заметок игроков: истории, детали + характера, визуальные свойства. Для их публикации нужны купоны. +

@@ -68,7 +80,11 @@

Квесты

-

Игроки выполняют разнообразные задания в игре, используя доступные команды. Квесты будем создавать вместе. Для их публикации нужны купоны.

+

+ Игроки выполняют разнообразные задания в игре, используя доступные + команды. Квесты будем создавать вместе. Для их публикации нужны + купоны. +

@@ -76,127 +92,132 @@

Игра за персонажа

-

Можно "арендовать" игрового персонажа на время: откроется доступ к странице, постам и активностям. Стоимость аренды динамичная и измеряется в купонах.

+

+ Можно "арендовать" игрового персонажа на время: откроется доступ к + странице, постам и активностям. Стоимость аренды динамичная и + измеряется в купонах. +

+ .mt-4 { + margin-top: 1.5em; + } + diff --git a/src/routes/[lang]/(website)/p/[userName]/+page.svelte b/src/routes/[lang]/(website)/p/[userName]/+page.svelte index 7342a0d8..986f92aa 100644 --- a/src/routes/[lang]/(website)/p/[userName]/+page.svelte +++ b/src/routes/[lang]/(website)/p/[userName]/+page.svelte @@ -119,7 +119,7 @@ color: #fff; font-weight: 700; font-size: 0.8rem; - background: var(--color-text); + background: var(--bronze-12); padding: 0 0.4em; border-radius: 50%; } diff --git a/src/routes/[lang]/(website)/trophy/[id]/+page.svelte b/src/routes/[lang]/(website)/trophy/[id]/+page.svelte index a80227fc..0ea87cdd 100644 --- a/src/routes/[lang]/(website)/trophy/[id]/+page.svelte +++ b/src/routes/[lang]/(website)/trophy/[id]/+page.svelte @@ -11,12 +11,16 @@ const timeAgo = new TimeAgo('ru-RU') - const latestProfiles = data.trophy.progress.filter((t) => t.status === 'COMPLETED').slice(0, 12) + const latestProfiles = data.trophy.progress + .filter((t) => t.status === 'COMPLETED') + .slice(0, 12)

{data.trophy.name}

-

Трофей, созданный hmbanan666

+

+ Трофей, созданный hmbanan666 +

@@ -56,101 +60,101 @@
From 77a8f31caf303313c1343480ab82012f3751a0cc Mon Sep 17 00:00:00 2001 From: Nick Kosarev Date: Wed, 3 Jul 2024 19:11:00 +0000 Subject: [PATCH 2/4] feat: new character page and new currency --- package.json | 2 +- src/lib/assets/website/coin-128.png | Bin 0 -> 765 bytes src/lib/assets/website/coin-16.png | Bin 0 -> 321 bytes src/lib/assets/website/coin-32.png | Bin 0 -> 383 bytes src/lib/assets/website/coin-64.png | Bin 0 -> 540 bytes src/lib/components/MenuDesktop.svelte | 46 +++++- src/lib/components/Profile.svelte | 2 +- src/lib/styles/styles.css | 4 +- .../[lang]/(website)/character/+page.svelte | 68 ++++++++ .../(website)/character/[id]/+page.svelte | 6 +- .../(website)/character/new/+page.svelte | 154 ++++++++++++++++++ .../[lang]/(website)/coupon/+page.svelte | 32 +++- .../(website)/p/[userName]/+page.svelte | 35 +++- .../[lang]/(website)/trophy/[id]/+page.svelte | 4 +- src/routes/[lang]/+layout.server.ts | 12 ++ yarn.lock | 8 +- 16 files changed, 349 insertions(+), 24 deletions(-) create mode 100644 src/lib/assets/website/coin-128.png create mode 100644 src/lib/assets/website/coin-16.png create mode 100644 src/lib/assets/website/coin-32.png create mode 100644 src/lib/assets/website/coin-64.png create mode 100644 src/routes/[lang]/(website)/character/new/+page.svelte diff --git a/package.json b/package.json index 01fe2c51..a693a190 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "prepare": "husky" }, "dependencies": { - "@hmbanan666/chat-game-api": "^0.1.15", + "@hmbanan666/chat-game-api": "^0.1.17", "@paralleldrive/cuid2": "^2.2.2", "@radix-ui/colors": "^3.0.0", "@twurple/api": "^7.1.0", diff --git a/src/lib/assets/website/coin-128.png b/src/lib/assets/website/coin-128.png new file mode 100644 index 0000000000000000000000000000000000000000..bc0ff4fdfcd3ec1a02e356763460ec840b24b60a GIT binary patch literal 765 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>VA}5K;uumf z=j|PP@2i0l2R{Bb7A!fnP*LVjv9uIZVef&XXBR475-<2J@a{rHFJF@d*S>}zf%Z7Z z2M?5Tb@?t{kkDf_y}`|Ku6f??ioCl0^S|Dizn{6zvOw->+1_Knb~mrSR-N%O>Ggu_ zpZ_|i>4#UdF{CpTAc&Xc7hcT!|L|wYH?d>4j-6cpsOsCC1HugF7#{E-5$k^SN#6Y& zzt;cizDIu5W|ixA%(GShz|vsMV8M(;aKE2$^ZTE<>y1Ot_wRoA=lrVYo1e4HL$(fV zaqL@L``>c)H6>v&^*<`izW%+*cm4UQdF$s2S1=urWSGN}!|A`D#KO()i(Ef{f4}t8 zvUPv|-TN2y>UVY}h7X?B+aGwn{OQ*RznK^P|7|a?6I1T3EP7T_OyB1o}a!iyyN@F^1{EsDE?vn4Kqrf zGYCFsDc$t`ry!u` z_CAJ-v$~${H}tvni+j?9gr*}>O=~3IF(vUjUhhd{I3rM_=WzZUgRfDXgz<0DWx!xy N@O1TaS?83{1OUFdep~aQqhX>ckFzGrm#Kox8y`cmT1A8H$f2{ADNekE4D6;isDGp{K5E#Iq9LgfKE=+ z+}_rR2rnsC-7Tfh3$E|H`K0p9pIs;K>h>Q$bGq}=BZX$r!aNG zrX;ontP2v)^UwXc`Rc=xEUpWCs=jCCsjYa=V9cP);9C;+;!M6-Th;Pq4Bk^6*FQ^Z z`o(9!e1Yl0lV{D=pV|GDJ5^gV{r9r2FE)0pWuL*A!I-h>#+l;uKc60*Tdn@=7?;;? z>F2#CAAF5~8`=8rz}%mA|31@yIvnA{K;_`QY_|DV?b;K7N=1n)VZn5ru$n#*)KIStFTNq4E8ZU_Z zJU!;djgPO+y_Czg{k6wBPIJj?^DE^IVAKBFCRATsfATxyuXpz|Lw~NSv-3Af-c_v} z)eke_(RYSl`{T`^8q{{VzbLNTQlBN{@LfPQw!UKPTR|nTM{iWO{*P8GNS_DSXzcv= s9Lviy!BDMkCGW0TT3g3dfgN_=>`=FVdQ&MBb@0H~w%VgLXD literal 0 HcmV?d00001 diff --git a/src/lib/components/MenuDesktop.svelte b/src/lib/components/MenuDesktop.svelte index 34663a81..3398484a 100644 --- a/src/lib/components/MenuDesktop.svelte +++ b/src/lib/components/MenuDesktop.svelte @@ -1,9 +1,12 @@
- +
+ {#if profile?.coins > 0} +
+
{profile.coins}
+
+ {/if} + + {#if profile?.coupons > 0} +
+
{profile.coupons}
+
+ {/if} + + +
diff --git a/src/lib/components/Profile.svelte b/src/lib/components/Profile.svelte index f13ac03c..56849161 100644 --- a/src/lib/components/Profile.svelte +++ b/src/lib/components/Profile.svelte @@ -58,7 +58,7 @@ .wrapper { position: relative; width: fit-content; - margin-left: auto; + display: inline-block; } .twitch { diff --git a/src/lib/styles/styles.css b/src/lib/styles/styles.css index ebd13d2f..4b139844 100644 --- a/src/lib/styles/styles.css +++ b/src/lib/styles/styles.css @@ -2,8 +2,10 @@ @import '@radix-ui/colors/bronze.css'; @import '@radix-ui/colors/brown.css'; +@import '@radix-ui/colors/gray.css'; @import '@radix-ui/colors/green.css'; @import '@radix-ui/colors/orange.css'; +@import '@radix-ui/colors/violet.css'; :root { --color-twitch: #8E4EC6; @@ -13,7 +15,7 @@ --color-border-2: #E1DCCF; --color-bg-accent-1: #56468B; --color-bg-accent-2: #6F5F58; - --color-common: #BBBBBB; + --color-common: var(--gray-9); --color-uncommon: #33B074; --color-rare: #0090FF; --color-epic: #7D66D9; diff --git a/src/routes/[lang]/(website)/character/+page.svelte b/src/routes/[lang]/(website)/character/+page.svelte index 66645203..f748d141 100644 --- a/src/routes/[lang]/(website)/character/+page.svelte +++ b/src/routes/[lang]/(website)/character/+page.svelte @@ -1,5 +1,6 @@ @@ -8,6 +9,18 @@

Игровые персонажи

Игроки сами создают и "прокачивают" персонажей. Можно "арендовать" на неделю, получить доступ к странице, создавать посты и делать активности.

+ +
+
+ +
+

{$page.data.profileData.coins}

+

Монет

+
+
+ + Создать нового [в разработке] +
@@ -87,4 +100,59 @@ .block .info p { font-weight: 500; } + + .currency-block { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: space-between; + gap: 0.5em; + margin-top: 2em; + background-color: var(--bronze-3); + border: 2px solid var(--bronze-5); + padding: 1em; + + & .currency { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: 0.75em; + + & .right { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: flex-start; + justify-content: center; + + & .counter { + font-size: 1.5rem; + font-weight: 600; + line-height: 1.2; + color: var(--green-10); + } + + & .description { + font-size: 0.9rem; + } + } + } + + & a { + padding: 0.5em 1em; + border: 2px solid var(--brown-7); + background-color: var(--violet-10); + color: var(--brown-3); + font-weight: 600; + transition: all 0.2s; + + &:hover { + opacity: 0.9; + text-decoration: none; + } + } + } diff --git a/src/routes/[lang]/(website)/character/[id]/+page.svelte b/src/routes/[lang]/(website)/character/[id]/+page.svelte index a3c4efdb..844fcba1 100644 --- a/src/routes/[lang]/(website)/character/[id]/+page.svelte +++ b/src/routes/[lang]/(website)/character/[id]/+page.svelte @@ -7,8 +7,8 @@
-

{data.character.name}

-

Игровой персонаж, созданный valentina8177

+

"{data.character.nickname}" {data.character.name}

+

Игровой персонаж, созданный {data.character.profile.userName}

Сейчас под управлением hmbanan666
@@ -21,7 +21,7 @@
-

Малый

+

Обычный

Уровень проработанности

diff --git a/src/routes/[lang]/(website)/character/new/+page.svelte b/src/routes/[lang]/(website)/character/new/+page.svelte new file mode 100644 index 00000000..e85cf051 --- /dev/null +++ b/src/routes/[lang]/(website)/character/new/+page.svelte @@ -0,0 +1,154 @@ + + +
+

Создание нового персонажа

+

Не торопись. Заполни все поля и нажми "Создать".

+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ + +
+
+ +
+

Правила публикации контента

+ +

1. Контент не должен содержать оскорбительных или дискриминационных элементов.

+

2. Избегайте копирования других игровых персонажей.

+

3. Не включайте политические темы, религиозные убеждения или социальные комментарии.

+
+ + diff --git a/src/routes/[lang]/(website)/coupon/+page.svelte b/src/routes/[lang]/(website)/coupon/+page.svelte index a80adc81..469ff82f 100644 --- a/src/routes/[lang]/(website)/coupon/+page.svelte +++ b/src/routes/[lang]/(website)/coupon/+page.svelte @@ -1,7 +1,7 @@
diff --git a/src/routes/[lang]/+layout.server.ts b/src/routes/[lang]/+layout.server.ts index b37d0943..f86fdb02 100644 --- a/src/routes/[lang]/+layout.server.ts +++ b/src/routes/[lang]/+layout.server.ts @@ -1,5 +1,6 @@ import { redirect } from '@sveltejs/kit' import { dictionary, supportedLocales } from '$lib/translations' +import { api } from '$lib/server/api' export async function load({ locals, params }) { const locale = locals.locale @@ -8,8 +9,19 @@ export async function load({ locals, params }) { redirect(301, `/${locale}`) } + let profileData = null + if (locals.profile?.userName) { + const profile = await api.profile.getByUserName(locals.profile.userName) + if (!profile || profile instanceof Error) { + profileData = null + } else { + profileData = profile + } + } + return { locale, t: dictionary(locale), + profileData, } } diff --git a/yarn.lock b/yarn.lock index 8bf10775..43dc3b54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -528,10 +528,10 @@ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== -"@hmbanan666/chat-game-api@^0.1.15": - version "0.1.15" - resolved "https://registry.yarnpkg.com/@hmbanan666/chat-game-api/-/chat-game-api-0.1.15.tgz#4ca76ac95aa9e96554eef238de191bc02b13e742" - integrity sha512-NEehogrHYFrf2U/V+b8L7dRDXvhHZRoNdKUj2C4JHk1/EZjQEO3cD2+nVZGnQbvbi+VKP+4A2ofg2EsgihH4qg== +"@hmbanan666/chat-game-api@^0.1.17": + version "0.1.17" + resolved "https://registry.yarnpkg.com/@hmbanan666/chat-game-api/-/chat-game-api-0.1.17.tgz#84963bc8d64f287e539fce9e077b809f234ef8fc" + integrity sha512-kfhRAqQ2V4nKWyuc2jndTd5ZC/9HpLynr3gE4KJHfGF5UUz0HkR6erqbYQfBpN3M/pH/QRFiLah8bkeBBUzifg== "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" From b75dbfdc80d7ea48441fdb76de752d9ee89bf1d7 Mon Sep 17 00:00:00 2001 From: Nick Kosarev Date: Fri, 5 Jul 2024 10:44:33 +0000 Subject: [PATCH 3/4] chore: some logic on form --- .../(website)/character/new/+page.svelte | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/routes/[lang]/(website)/character/new/+page.svelte b/src/routes/[lang]/(website)/character/new/+page.svelte index e85cf051..1c4b873f 100644 --- a/src/routes/[lang]/(website)/character/new/+page.svelte +++ b/src/routes/[lang]/(website)/character/new/+page.svelte @@ -1,16 +1,34 @@ -

Создание нового персонажа

-

Не торопись. Заполни все поля и нажми "Создать".

+

Не торопись. Заполни все поля и ознакомься с правилами.

- +
@@ -22,12 +40,12 @@
- +
-
@@ -120,7 +138,7 @@ width: 100%; padding: 0.75em 1em; border: 2px solid var(--bronze-7); - background-color: var(--green-9); + background-color: var(--gray-9); color: var(--bronze-3); font-family: inherit; font-size: 1.2rem; @@ -141,6 +159,14 @@ font-size: 0.9rem; text-transform: none; } + + &[data-ready=true] { + background-color: var(--green-9); + } + + &:disabled { + cursor: not-allowed; + } } .terms { From 50500124a8f94e10e6d6ad31d585869216e17fd1 Mon Sep 17 00:00:00 2001 From: Nick Kosarev Date: Fri, 5 Jul 2024 12:36:46 +0000 Subject: [PATCH 4/4] feat: plugin for stream --- .../stream-plugin/customWebSocketService.ts | 85 ++++ .../game/stream-plugin/unitsOnStreamPlugin.ts | 369 ++++++++++++++++++ src/lib/game/types.ts | 20 + .../[lang]/(game)/stream/+layout.svelte | 17 + src/routes/[lang]/(game)/stream/+page.svelte | 45 +++ 5 files changed, 536 insertions(+) create mode 100644 src/lib/game/stream-plugin/customWebSocketService.ts create mode 100644 src/lib/game/stream-plugin/unitsOnStreamPlugin.ts create mode 100644 src/routes/[lang]/(game)/stream/+layout.svelte create mode 100644 src/routes/[lang]/(game)/stream/+page.svelte diff --git a/src/lib/game/stream-plugin/customWebSocketService.ts b/src/lib/game/stream-plugin/customWebSocketService.ts new file mode 100644 index 00000000..e3dc0754 --- /dev/null +++ b/src/lib/game/stream-plugin/customWebSocketService.ts @@ -0,0 +1,85 @@ +import type { WebSocketMessage } from '@hmbanan666/chat-game-api' +import type { UnitsOnStream } from '$lib/game/types' +import { browser } from '$app/environment' +import { config } from '$lib/config' + +export class CustomWebSocketService { + socket!: WebSocket + messagesPerSecond = 0 + kbitPerSecond = 0 + game: UnitsOnStream + + constructor(game: UnitsOnStream) { + this.game = game + + if (browser && this.game.options.isSocketOn) { + this.#init() + } + } + + update() {} + + async #handleMessage(message: WebSocketMessage) { + if (message.type === 'MESSAGE') { + const { text, player } = message.data + this.game.handleMessage({ text, playerId: player.id }) + } + // if (message.type === 'RAID_STARTED') { + // this.game.audio.playSound('MARCHING_WITH_HORNS') + // } + // if (message.type === 'GROUP_FORM_STARTED') { + // this.game.audio.playSound('MARCHING_WITH_HORNS') + // } + // if (message.type === 'MAIN_QUEST_STARTED') { + // this.game.audio.playSound('MARCHING_WITH_HORNS') + // } + // if (message.type === 'SCENE_CHANGED') { + // this.game.rebuildScene() + // } + // if (message.type === 'IDEA_CREATED') { + // this.game.audio.playSound('YEAH') + // } + } + + #init() { + this.socket = new WebSocket(config.websocketUrl ?? '', [this.game.id]) + + this.#setMessagesPerSecondHandler() + + this.socket.addEventListener('open', () => { + this.socket.send(JSON.stringify({ type: 'GAME_HANDSHAKE', id: this.game.id, profileJWT: this.game.profileJWT })) + }) + + this.socket.addEventListener('message', (event) => { + const message = this.#parse(event.data.toString()) + if (!message) { + return + } + + this.messagesPerSecond += 1 + const bytes = JSON.stringify(message).length + this.kbitPerSecond += Math.round((bytes * 8) / 1024) + + void this.#handleMessage(message) + }) + } + + #parse(message: string): WebSocketMessage | undefined { + const parsed = JSON.parse(message) + if (parsed) { + return parsed as WebSocketMessage + } + + return undefined + } + + #setMessagesPerSecondHandler() { + return setInterval(() => { + // console.log( + // `${this.messagesPerSecond} msg/s, ${this.kbitPerSecond} kbit/s`, + // ) + this.messagesPerSecond = 0 + this.kbitPerSecond = 0 + }, 1000) + } +} diff --git a/src/lib/game/stream-plugin/unitsOnStreamPlugin.ts b/src/lib/game/stream-plugin/unitsOnStreamPlugin.ts new file mode 100644 index 00000000..fe22e400 --- /dev/null +++ b/src/lib/game/stream-plugin/unitsOnStreamPlugin.ts @@ -0,0 +1,369 @@ +import { Application, Container } from 'pixi.js' +import { createId } from '@paralleldrive/cuid2' +import { gameOptions } from '../store.svelte' +import { ServerService } from '../services/server/serverService' +import { BaseWagon } from '../objects/baseWagon' +import { FlagObject } from '../objects/flagObject' +import { ANSWER } from '../services/action/answer' +import { Player } from '../objects/units/player' +import { MoveOffScreenAndSelfDestroyScript } from '../scripts/moveOffScreenAndSelfDestroyScript' +import { CustomWebSocketService } from './customWebSocketService' +import type { + Game, + GameObject, + GameObjectPlayer, + GameOptions, + IGameActionResponse, + UnitsOnStream, +} from '$lib/game/types' +import { AssetsManager } from '$lib/game/utils/assetsManager' +import { getRandomInRange } from '$lib/utils/random' +import { MoveToTargetScript } from '$lib/game/scripts/moveToTargetScript' +import { EventService } from '$lib/game/services/event/eventService' +import { Group } from '$lib/game/common/group' +import { PlayerService } from '$lib/game/services/player/playerService' +import type { Wagon } from '$lib/game/services/wagon/interface' +import { getDateMinusMinutes } from '$lib/utils/date' + +interface UnitsOnStreamPluginOptions { + isSocketOn?: boolean +} + +export class UnitsOnStreamPlugin extends Container implements UnitsOnStream { + id: string + profileJWT?: string + options: GameOptions + children: Game['children'] = [] + app: Application + scene!: Game['scene'] + tick: Game['tick'] = 0 + group: Group + wagon!: BaseWagon + + eventService: EventService + playerService: PlayerService + websocketService: CustomWebSocketService + serverService: ServerService + + #outFlags: FlagObject[] = [] + #nearFlags: FlagObject[] = [] + + #cameraX = 0 + #cameraY = 0 + #cameraPerfectX = 0 + #cameraPerfectY = 0 + + constructor({ isSocketOn }: UnitsOnStreamPluginOptions) { + super() + + this.options = gameOptions + this.options.isSocketOn = isSocketOn ?? false + + this.id = createId() + this.app = new Application() + this.group = new Group() + + this.eventService = new EventService(this as any) + this.playerService = new PlayerService(this as any) + this.websocketService = new CustomWebSocketService(this) + this.serverService = new ServerService(this as any) + } + + async init() { + await this.app.init({ + backgroundAlpha: 0, + antialias: false, + roundPixels: false, + resolution: 1, + resizeTo: window, + }) + + await AssetsManager.init() + + // this.audio.playSound("FOREST_BACKGROUND") + + this.#initWagon({ x: 1, y: 1 }) + + this.app.stage.addChild(this) + + // this.initScene('MOVING') + + this.app.ticker.add(() => { + if (this.options.isPaused) { + return + } + + this.tick = this.app.ticker.FPS + + this.eventService.update() + // this.wagonService.update() + this.#removeInactivePlayers() + this.#updateObjects() + this.#removeDestroyedObjects() + + this.#changeCameraPosition(this.wagon) + this.#moveCamera() + }) + } + + async play() { + this.options.isPaused = false + + // setInterval(() => { + // console.log("FPS", this.app.ticker.FPS) + // console.log("Objects", this.children.length) + // }, 1000) + } + + destroy() { + this.app.destroy() + + super.destroy() + } + + removeObject(obj: GameObject) { + this.removeChild(obj) + } + + get activePlayers() { + return this.children.filter((obj) => obj.type === 'PLAYER') as GameObjectPlayer[] + } + + checkIfThisFlagIsTarget(id: string): boolean { + for (const obj of this.children) { + if (obj.target?.id === id) { + return true + } + } + return false + } + + findObject(id: string): GameObject | undefined { + return this.children.find((obj) => obj.id === id) + } + + rebuildScene(): void { + this.removeChild(...this.children) + } + + get randomOutFlag() { + return this.#outFlags[Math.floor(Math.random() * this.#outFlags.length)] + } + + get randomNearFlag() { + return this.#nearFlags[Math.floor(Math.random() * this.#nearFlags.length)] + } + + #initWagon({ x, y }: { x: number, y: number }) { + this.wagon = new BaseWagon({ game: this as any, x, y }) + this.wagon.init() + + this.#initOutFlags() + this.#initNearFlags() + } + + #initOutFlags(count = 1) { + for (let i = 0; i < count; i++) { + this.#outFlags.push(this.#generateRandomOutFlag()) + } + } + + #initNearFlags(count = 20) { + for (let i = 0; i < count; i++) { + this.#nearFlags.push(this.#generateRandomNearFlag()) + } + } + + #generateRandomOutFlag() { + const offsetX = -500 + const offsetY = 30 + + const flag = new FlagObject({ + game: this as any, + variant: 'OUT_OF_SCREEN', + x: this.wagon.x + offsetX, + y: this.wagon.y + offsetY, + offsetX, + offsetY, + }) + + return flag + } + + #generateRandomNearFlag() { + const minOffsetX = 200 + + const offsetX + = getRandomInRange(minOffsetX, this.app.screen.width - 500) + const offsetY = 30 + + const flag = new FlagObject({ + game: this as any, + variant: 'WAGON_NEAR_MOVEMENT', + x: this.wagon.x + offsetX, + y: this.wagon.y + offsetY, + offsetX, + offsetY, + }) + + return flag + } + + async handleMessage({ playerId, text }: { playerId: string, text: string }): Promise { + const player = await this.#initPlayer(playerId) + if (!player) { + return ANSWER.NO_PLAYER_ERROR + } + + return this.#handleMessage(player, text) + } + + async #initPlayer(id: string) { + const player = await this.findOrCreatePlayer(id) + if (!player) { + return + } + + this.group.join(player) + this.addChild(player) + player.updateLastActionAt() + + return player + } + + async findOrCreatePlayer(id: string): Promise { + const player = this.#findPlayer(id) + if (!player) { + return this.#createPlayer(id) + } + + return player + } + + #findPlayer(id: string) { + return this.children.find((p) => p.id === id && p.type === 'PLAYER') as Player | undefined + } + + async #createPlayer(id: string) { + const player = new Player({ game: this as any, id, x: -100, y: -100 }) + await player.init() + + const flag = this.randomOutFlag + player.x = flag.x + player.y = flag.y + + return player + } + + #removeInactivePlayers() { + for (const player of this.activePlayers) { + const checkTime = getDateMinusMinutes(8) + if (player.lastActionAt.getTime() <= checkTime.getTime()) { + if (player.script) { + continue + } + + const target = this.randomOutFlag + const selfDestroyFunc = () => { + this.group.remove(player) + } + + player.script = new MoveOffScreenAndSelfDestroyScript({ + target, + object: player, + selfDestroyFunc, + }) + } + } + } + + async #handleMessage(player: GameObjectPlayer, text: string): Promise { + player.addMessage(text) + + return ANSWER.OK + } + + #updateObjects() { + for (const object of this.children) { + object.animate() + object.live() + + if (object.type === 'PLAYER') { + this.#updatePlayer(object as GameObjectPlayer) + } + } + } + + #updatePlayer(object: GameObjectPlayer) { + if (object.script) { + return + } + + if (object.state === 'IDLE') { + const random = getRandomInRange(1, 150) + if (random <= 1) { + const target = this.randomNearFlag + + object.script = new MoveToTargetScript({ + object, + target, + }) + } + } + } + + #removeDestroyedObjects() { + for (const object of this.children) { + if (object.state === 'DESTROYED') { + const index = this.children.indexOf(object) + this.children.splice(index, 1) + return + } + } + } + + #changeCameraPosition(wagon: Wagon) { + const columnWidth = this.app.screen.width / 8 + const rowHeight = this.app.screen.height / 8 + + const leftPadding = columnWidth - 20 + const topPadding = rowHeight * 7 - 5 + + this.#cameraPerfectX = -wagon.x + leftPadding + this.#cameraPerfectY = -wagon.y + topPadding + + // If first load + if (Math.abs(-wagon.x - this.#cameraX) > 3000) { + this.#cameraX = this.#cameraPerfectX + } + if (Math.abs(-wagon.y - this.#cameraY) > 3000) { + this.#cameraY = this.#cameraPerfectY + } + } + + #moveCamera() { + const cameraSpeedPerSecond = 25 + const cameraDistance = cameraSpeedPerSecond / this.tick + + const bufferX = Math.abs(this.#cameraPerfectX - this.#cameraX) + const moduleX = this.#cameraPerfectX - this.#cameraX > 0 ? 1 : -1 + const addToX = bufferX > cameraDistance ? cameraDistance : bufferX + + if (this.#cameraX !== this.#cameraPerfectX) { + this.#cameraX += addToX * moduleX + } + + const bufferY = Math.abs(this.#cameraPerfectY - this.#cameraY) + const moduleY = this.#cameraPerfectY - this.#cameraY > 0 ? 1 : -1 + const addToY = bufferY > cameraDistance ? cameraDistance : bufferY + + if (this.#cameraY !== this.#cameraPerfectY) { + this.#cameraY += addToY * moduleY + } + + if (this.parent) { + this.parent.x = this.#cameraX + this.parent.y = this.#cameraY + } + } +} diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index a4940cfa..f590ec22 100644 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -22,6 +22,26 @@ import type { import type { GameActionService } from '$lib/game/services/action/interface' import type { GameQuestService } from '$lib/game/services/quest/interface' +export interface UnitsOnStream extends Container { + id: string + profileJWT?: string + options: GameOptions + children: GameObject[] + tick: number + scene: GameScene + group: IGameGroup + activePlayers: GameObjectPlayer[] + eventService: GameEventService + playerService: GamePlayerService + serverService: GameServerService + play: () => void + checkIfThisFlagIsTarget: (id: string) => boolean + findObject: (id: string) => GameObject | undefined + removeObject: (obj: GameObject) => void + rebuildScene: () => void + handleMessage: ({ playerId, text }: { playerId: string, text: string }) => Promise +} + export interface Game extends Container { id: string profileJWT?: string diff --git a/src/routes/[lang]/(game)/stream/+layout.svelte b/src/routes/[lang]/(game)/stream/+layout.svelte new file mode 100644 index 00000000..e59ac339 --- /dev/null +++ b/src/routes/[lang]/(game)/stream/+layout.svelte @@ -0,0 +1,17 @@ + + +
+ +
+ + diff --git a/src/routes/[lang]/(game)/stream/+page.svelte b/src/routes/[lang]/(game)/stream/+page.svelte new file mode 100644 index 00000000..8bbeb1de --- /dev/null +++ b/src/routes/[lang]/(game)/stream/+page.svelte @@ -0,0 +1,45 @@ + + +
+
+
+ +