Compare commits

...

20 commits

Author SHA1 Message Date
6907404c67 fixes 2024-11-30 14:27:40 +04:00
b3930f77c8 Merge pull request 'fixes' (#7) from bugfix/review-text-before-release into main
Reviewed-on: #7
2024-11-30 12:58:59 +04:00
352260c86f fixes 2024-11-30 12:58:45 +04:00
a7553e0614 fixes 2024-11-30 12:56:48 +04:00
0aad9d3ecb Merge pull request '2024 edition initial' (#1) from 2024edition into main
Reviewed-on: #1
2024-11-28 01:33:28 +04:00
ad965cfd6a fixes 2024-11-28 01:31:02 +04:00
5dd911fb01 docker support & updated texts 2024-11-27 21:20:02 +04:00
70cd2a8587 Merge pull request 'TGD-52' (#6) from TGD-52 into 2024edition
Reviewed-on: #6
2024-11-27 13:39:01 +04:00
98fe79c396 TGD-52 2024-11-27 13:38:18 +04:00
29dc437cc8 fix final caption 2024-11-27 13:13:24 +04:00
9710f7ca4e fix final caption 2024-11-26 01:14:18 +04:00
b4f7776510 fix final caption 2024-11-26 01:08:54 +04:00
6bd59bdee6 add finish caption 2024-11-26 00:42:58 +04:00
136d449b1b fix qr 2024-11-26 00:16:56 +04:00
2a32ca8e0c Merge pull request 'TGD-29: New qr code' (#5) from features/TGD-29 into 2024edition
Reviewed-on: #5
2024-11-26 00:14:25 +04:00
155ee06cb5 TGD-29: New qr code 2024-11-26 00:13:45 +04:00
231c412576 Merge pull request 'Update punishments.json' (#3) from netizen92-patch-2 into 2024edition
Reviewed-on: #3
Reviewed-by: webster <kirill@ngweb.io>
2024-11-24 22:56:23 +04:00
c9328cc115 Merge branch '2024edition' into netizen92-patch-2 2024-11-24 22:55:33 +04:00
1395648444 Merge pull request 'TGD-58' (#4) from feature/TGD-58 into 2024edition
Reviewed-on: #4
2024-11-24 22:45:40 +04:00
7e493f4bc3 Update punishments.json
updated punishment list
2024-11-24 18:32:15 +04:00
34 changed files with 1390 additions and 491 deletions

32
Dockerfile Normal file
View file

@ -0,0 +1,32 @@
# Stage 1: Build the Angular application
FROM node:20 as build
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json to install dependencies
COPY package*.json ./
# Install Node.js dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the Angular application in production mode
RUN npm run build --configuration=prod
# Stage 2: Serve the app with Nginx
FROM nginx:1.21-alpine
# Copy the built app from the previous stage
COPY --from=build /app/dist/thanksgiving /usr/share/nginx/html
# Copy the custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]

156
gift.json
View file

@ -1,156 +0,0 @@
[{
"prizeID": 1,
"name": "Тесто для лепки невкусное",
"isGifted": false
},
{
"prizeID": 2,
"name": "Палочки с ароматом лучших публичных домов Бангкока",
"isGifted": false
},
{
"prizeID": 3,
"name": "2 метра хюгге",
"isGifted": false
},
{
"prizeID": 4,
"name": "Палку светящуюся бесполезную",
"isGifted": false
},
{
"prizeID": 5,
"name": "Тёрку для лилипутов",
"isGifted": false
},
{
"prizeID": 6,
"name": "Мёртвую белочку",
"isGifted": false
},
{
"prizeID": 7,
"name": "Лучшего друга Спанч боба засушенного",
"isGifted": false
},
{
"prizeID": 8,
"name": "Подарок для любителей помесить глину",
"isGifted": false
},
{
"prizeID": 9,
"name": "Палку чесательную полезную",
"isGifted": false
},
{
"prizeID": 10,
"name": "Красного петуха - своё тотемное животное",
"isGifted": false
},
{
"prizeID": 11,
"name": "Набор свечей романтишный",
"isGifted": false
},
{
"prizeID": 12,
"name": "Хранилище для денег патриотическое",
"isGifted": false
},
{
"prizeID": 13,
"name": "Мерч от Каца",
"isGifted": false
},
{
"prizeID": 14,
"name": "Чупа-чупс со вкусом патриотизма",
"isGifted": false
},
{
"prizeID": 15,
"name": "Тренажеры для легких разноцветные",
"isGifted": false
},
{
"prizeID": 16,
"name": "Паззл предсказуемый",
"isGifted": false
},
{
"prizeID": 17,
"name": "Жопный блокнот",
"isGifted": false
},
{
"prizeID": 18,
"name": "Носки от батьки праздничные",
"isGifted": false
},
{
"prizeID": 19,
"name": "Носки женские миленькие",
"isGifted": false
},
{
"prizeID": 20,
"name": "Набор художника-нумеролога",
"isGifted": false
},
{
"prizeID": 21,
"name": "Карандаш вечный как Путин",
"isGifted": false
},
{
"prizeID": 22,
"name": "Массажёр для жопы",
"isGifted": false
},
{
"prizeID": 23,
"name": "Сладкий подарок рот в рот",
"isGifted": false
},
{
"prizeID": 24,
"name": "Мотоцикл (ненастоящий)",
"isGifted": false
},
{
"prizeID": 25,
"name": "Вышивку для эскортниц (алмазную)",
"isGifted": false
},
{
"prizeID": 26,
"name": "Звенящие бубенцы",
"isGifted": false
},
{
"prizeID": 27,
"name": "Спонж для умывания твоих кислых щей",
"isGifted": false
},
{
"prizeID": 28,
"name": "Мочалку с портретом дракона",
"isGifted": false
},
{
"prizeID": 29,
"name": "Тетрадь для чётких квадроберов",
"isGifted": false
},
{
"prizeID": 30,
"name": "Костюм для руки незнакомки эротишный",
"isGifted": false
},
{
"prizeID": 31,
"name": "Плакат с кумиром детства нарядный",
"isGifted": false
}
]

11
nginx.conf Normal file
View file

@ -0,0 +1,11 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

5
package-lock.json generated
View file

@ -34,7 +34,7 @@
"@angular/cli": "^16.2.9",
"@angular/compiler-cli": "~16.2.12",
"@types/jasmine": "~3.8.0",
"@types/node": "^12.11.1",
"@types/node": "^12.20.55",
"jasmine-core": "~3.8.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
@ -3646,7 +3646,8 @@
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/@types/node-forge": {
"version": "1.3.9",

View file

@ -36,7 +36,7 @@
"@angular/cli": "^16.2.9",
"@angular/compiler-cli": "~16.2.12",
"@types/jasmine": "~3.8.0",
"@types/node": "^12.11.1",
"@types/node": "^12.20.55",
"jasmine-core": "~3.8.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",

View file

@ -1,170 +1,152 @@
[
{
"text": "Расскажите про свою самую любимую игрушку"
"text": "Сделать смешной комплимент каждому присутствующему"
},
{
"text": "Назовите 20 слов на букву Ч"
"text": "Назвать 20 слов на букву Ч"
},
{
"text": "Без слов изобразите то, чем приходится заниматься на работе, чтобы присутствующие угадали."
"text": "Рассказать две истории - правду и выдумку. Остальные должны угадать, где какая"
},
{
"text": "Изобразите 5 видов спорта так, чтобы присутствующие смогли их назвать."
"text": "Сосед ручкой рисует тебе татуировку на руке"
},
{
"text": "Выполните приседания (10 раз), положив на голову книгу."
"text": "Пусть кто-то из игроков сделает тебе новую прическу"
},
{
"text": "Посчитайте любую считалку, на ком она остановится, должен выпить с тобой"
"text": "Сделать себе патчи под глаза из огурца и сидеть так 5 минут"
},
{
"text": "Попросите каждого игрока по очереди назвать слово и придумать к нему рифму"
"text": "Стоя дотянуться коленкой до носа"
},
{
"text": "Назовите 5 грузинских вин"
"text": "Нарисовать себе усы"
},
{
"text": "Изобразите кота, которому страшно, но любопытно "
"text": "В течение минуты гладить себя одновременно по голове и по животу, но в разных направлениях"
},
{
"text": "Сделайте необычный подарок игроку с максимальным количеством очков, не выходя из комнаты"
"text": "Прочитать любое детское стихотворение с кавказским акцентом"
},
{
"text": "Нарисуйте или приклейте милые усики "
"text": "Сказать быстро 3 скороговорки"
},
{
"text": зобразите иностранца. Говорите на любом языке, можно даже на собственном."
"text": грать пантомиму \"Гопник\": сесть на корточки и щелкать семечки, можно потребовать мелочь у ближайших игроков"
},
{
"text": "Расскажите плохой анекдот"
"text": "Набрать в рот орехов и произнести 5 раз фразу \"толстощекий вкусноежка\""
},
{
"text": "Опишите свою работу тремя словами"
"text": "Съесть четверть лимона"
},
{
"text": "Распознайте на ощупь 5 разных предметов с завязанными глазами, конечно же."
"text": "Подпрыгивая на одной ноге, читать стихотворение"
},
{
"text": "Подпрыгните 10 раз, каждый раз произнося \"индейка\"."
"text": "Попрыгать по комнате как лягушка"
},
{
"text": "Расскажите стих, в котором будет ваше имя."
"text": "Выпить вино из блюдца без помощи рук"
},
{
"text": "Изобразите свой любимый фрукт без слов."
"text": "Написать свое имя на бумаге, держа карандаш в зубах"
},
{
"text": "Расскажите мини-историю о забавных приключениях вашей левой руки."
"text": "Открыть книгу на любой странице и прочитать отрывок похотливым голосом"
},
{
"text": ридумайте себе псевдоним и откликайтесь только на него следующие 5 минут."
"text": опытаться засунуть в рот кулак"
},
{
"text": "Изобразите муравья, который нашел огромную еду."
"text": "Выполнить приседания (10 раз), положив на голову книгу"
},
{
"text": "Нарисуйте свой знак зодиака, чтобы остальные игроки отгадали."
"text": "Посчитать любую считалку, на ком она остановится, должен выпить с тобой"
},
{
"text": "Подпевайте любимой песне, заменяя слова на \"ля-ля-ля\"."
"text": "Рассказать плохой анекдот"
},
{
"text": "Играйте в невидимую гитару и исполняйте короткую мелодию."
"text": "Распознать на ощупь 5 разных предметов (с завязанными глазами, конечно же)"
},
{
"text": редставьте, что вы робот, и произнесите что-то с использованием роботизированного голоса."
"text": одпрыгнуть 10 раз, каждый раз произнося \"индейка\""
},
{
"text": "Изобразите свой страх перед любым предметом в комнате."
"text": "Изобразить муравья, который нашел огромную еду"
},
{
"text": остарайтесь нарисовать свою любимую песню."
"text": редставить, что вы все находитесь на красной дорожке, и ты - главная звезда. Пройтись по комнате с гордой осанкой"
},
{
"text": "Изобразите смешное животное, которого нет в реальном мире."
"text": "Говорить как пират в течение пяти минут"
},
{
"text": "Перевоплотитесь в своего любимого персонажа книги или фильма и представьтесь."
"text": "Дотянуться языком до носа"
},
{
"text": "Назовите алфавит задом наперед."
"text": "Изображать известного мультяшного персонажа в течение 1 минуты"
},
{
"text": "Спойте отрывок из любимой детской песни."
"text": "Съесть кусочек хлеба, на который несколько игроков добавят один ингредиент по выбору"
},
{
"text": "Изобразите, что вы танцуете на льду, не поднимаясь с места."
"text": "Сфотографироваться в самой нелепой позе"
},
{
"text": "Постарайтесь сказать \"индейка\" наоборот."
"text": "Попытаться постоять на одной ноге 30 секунд с завязанными глазами"
},
{
"text": "Расскажите короткую историю, используя только по три слова в каждом предложении."
"text": "Исполнить короткий танец на месте"
},
{
"text": "Играйте в 'испорченный телефон': прошепчите любую фразу первому человеку, а затем посмотрите, как она изменится по цепочке."
"text": "Произнести алфавит, чередуя громкий голос и шёпот"
},
{
"text": "Назовите пять стран, начинающихся на букву \"И\"."
"text": "Сказать фразу из трёх слов, которая заставит всех засмеяться"
},
{
"text": "Переведите любое слово на вымышленный язык и объясните его значение."
"text": "Танцевать под песню, которую выберут другие"
},
{
"text": "Представьте, что вы новый супергерой с уникальной способностью, и расскажите о ней."
},
{
"text": "Издайте звук, который в вашем представлении соответствует слову 'веселье'."
"text": "Нарисовать кошку, не отрывая ручку от бумаги"
},
{
"text": "Расскажите короткую историю о приключениях своей тапочки."
"text": "Изобразить известного супергероя, но на пенсии"
},
{
"text": "Изобразите смешное лицо и попросите остальных угадать эмоцию."
"text": "Придумать и показать три способа необычного использования стула"
},
{
"text": "Придумайте по одному положительному качества для каждого игрока."
"text": "Изобразить, как вы идёте по раскалённым углям"
},
{
"text": "Представьте, что вы ведущий радиошоу и сделайте короткую передачу на любую тему."
"text": "Сказать комплимент каждому игроку, используя слова, начинающиеся на одну букву"
},
{
"text": остарайтесь сделать звуковое подражание своего любимого животного."
"text": ридумать стихотворение из 4 строк про самого себя. Рассказать с выражением"
},
{
"text": "Расскажите короткую историю о приключениях своего домашнего растения."
"text": "Провести импровизированный урок танцев, обучая всех \"новому движению\""
},
{
"text": "Представьтесь как профессиональный критик и дайте короткий обзор своего дня."
},
{
"text": "Представьте, что вы находитесь на красной дорожке, и вы - главная звезда. Пройдитесь по комнате с гордой осанкой."
"text": "Сказать скороговорку, зажав язык зубами"
},
{
"text": "Играйте в \"замедленное движение\": выполните простую задачу (например, открытие двери) медленно и торжественно."
"text": "Сделать из салфеток или бумаги шляпу и носить её до следующего наказания"
},
{
"text": "Говорите как пират в течение пяти минут."
"text": "\"атака роботов\": исполнить наказание, придуманное искусственным интеллектом в реальном времени"
},
{
"text": "Возьмите на себя роль человеческой статуи и замрите в забавной позе на пять минут."
},
{
"text": "Рассказать скороговорку без запинок, если запнулся, то начать заново."
},
{
"text": "Нарисовать монобровь."
},
{
"text": "Выпить или съесть что-то, не используя руки."
},
{
"text": "Дотянуться языком до носа."
},
{
"text": "Вылакать стаканчик сока или молока из блюдца."
},
{
"text": "Набить рот чем-то вкусненьким и произнести 5 раз фразу \"толстощекий вкуснооежка\"."
}
}
]

View file

@ -1,4 +0,0 @@
export const API_URL = 'http://127.0.0.1:3000';
export const WEBSOCK_URL = 'http://127.0.0.1:3000';
// export const API_URL = 'https://thanksgiving2023.ngweb.io/api';
//export const WEBSOCK_URL = "https://thanksgiving2023.ngweb.io/"

View file

@ -5,6 +5,7 @@
<h4>Game</h4>
<button class="btn btn-danger" (click)="clearGameQueue()">Clear queue</button>
<button class="btn btn-danger" (click)="simulateEndGamePoints()">Simulate endgame points</button>
<button class="btn btn-danger" (click)="simulateValidAnswer()">Simulate valid answer</button>
<h4>Versus</h4>
<button class="btn btn-danger" (click)="simulateVersus()">Begin versus</button>
<button class="btn btn-danger" (click)="resetAllVersusTasksAsIncompleted()">Mark all as uncompleted</button>

View file

@ -53,4 +53,8 @@ export class AdminTestingComponent implements OnInit, OnDestroy {
simulateEndGamePoints() {
this.testingApiService.simulateEndGamePoints().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r));
}
simulateValidAnswer() {
this.testingApiService.simulateValidAnswer().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r));
}
}

View file

@ -1,6 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { io, Socket } from "socket.io-client";
import { API_URL, WEBSOCK_URL } from '../app.constants';
import { EventService } from "./services/event.service";
import {EventStateChanged, ServerEvent, VersusBeginEvent} from "../types/server-event";
import { ApiService } from "./services/api.service";
@ -8,9 +7,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { filter, map, takeUntil } from "rxjs/operators";
import { ToastService } from "./toast.service";
import { VoiceService } from "./services/voice.service";
import { Subject } from "rxjs";
import {delay, delayWhen, Subject} from "rxjs";
import { getAudioPath } from "./helper/tts.helper";
import {animate, keyframes, style, transition, trigger} from "@angular/animations";
import {environment} from "../environments/environment";
@Component({
selector: 'app-root',
@ -32,7 +32,7 @@ import {animate, keyframes, style, transition, trigger} from "@angular/animation
})
export class AppComponent implements OnInit, OnDestroy {
title = 'thanksgiving';
connection = io(WEBSOCK_URL, { transports: ['websocket']});
connection = io(environment.WEBSOCK_URL, { transports: ['websocket']});
destroyed = new Subject<void>();
versusData: VersusBeginEvent|null = null;
audioSrc: string;
@ -53,8 +53,8 @@ export class AppComponent implements OnInit, OnDestroy {
console.log(data);
this.eventService.emit(data);
});
this.apiService.getAppState('main').subscribe((result) => {
if(this.router.url.indexOf('admin') === -1) {
this.apiService.getAppState('main').pipe(takeUntil(this.destroyed),delay(300)).subscribe((result) => {
if(this.router.url.indexOf('admin') === -1 || window.location.href.indexOf('admin') === -1) {
this.router.navigate([`/${result.value}`]).then(() => {
console.log(`navigated to ${result.value}`);
})

View file

@ -3,8 +3,10 @@
<div class="d-block justify-content-centers">
<h1 *ngIf="answerIsValid">🎉 Ура, правильный ответ!</h1>
<h1 *ngIf="!answerIsValid">А вот и нет! ❌</h1>
<div class="d-flex align-items-center justify-content-center">
<app-participant-item *ngIf="participant" [participant]="participant"></app-participant-item>
<div class="d-flex align-items-center justify-content-center flex-wrap" *ngIf="this.participants.length > 0">
<ng-container *ngFor="let participant of participants" >
<app-participant-item [participant]="participant"></app-participant-item>
</ng-container>
</div>
<h2 class="text-center" *ngIf="!answerIsValid">выйграл наказание</h2>
<audio *ngIf="!answerIsValid" src="assets/sfx/wrong_answer.mp3" autoplay></audio>

View file

@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ApiService } from "../../services/api.service";
import { interval, Observable, Subject } from "rxjs";
import {concatMap, interval, Observable, Subject} from "rxjs";
import { EventService } from "../../services/event.service";
import { filter, map, take, takeUntil, tap } from "rxjs/operators";
import { Participant } from "../../../types/participant";
@ -51,14 +51,12 @@ import { VoiceService } from "../../services/voice.service";
export class AnswerNotificationComponent implements OnInit, OnDestroy {
isShown = false;
answerIsValid = false;
participant: Participant;
timer: Observable<any>;
countdown = 10;
showCountdown = false;
announceAudio = true;
participants: Participant[] = [];
audioSrc: string;
private destroyed$ = new Subject<void>();
constructor(private apiService: ApiService, private eventService: EventService, private voiceService: VoiceService) {
this.eventService.answerReceivedEvent.pipe(
takeUntil(this.destroyed$),
@ -68,20 +66,23 @@ export class AnswerNotificationComponent implements OnInit, OnDestroy {
takeUntil(this.destroyed$),
map(e => e.data)
).subscribe(d => this.showNotification(d.telegramId, false, d.validAnswer, null));
this.eventService.scoreChangedEvent.pipe(
takeUntil(this.destroyed$),
map(e => e.data),
).subscribe(e => {
if(e.telegramId === this.participant.telegramId) {
this.participant.score = e.newScore
}
});
// this.eventService.scoreChangedEvent.pipe(
// takeUntil(this.destroyed$),
// map(e => e.data),
// ).subscribe(e => {
// if(e.telegramId === this.participant.telegramId) {
// this.participant.score = e.newScore
// }
// });
}
showNotification(telegramId: number, validAnswer: boolean, validAnswerValue: string, note: string|null) {
this.countdown = validAnswer ? 10 : 5;
console.log(`showNotification`);
this.apiService.getParticipant(telegramId).subscribe(p => {
this.participant = p;
this.countdown = validAnswer ? 10 : 5;
this.participants.push(p);
this.isShown = true;
this.answerIsValid = validAnswer;
const template = validAnswer ? 'announce-valid' : 'announce-invalid';
@ -89,7 +90,10 @@ export class AnswerNotificationComponent implements OnInit, OnDestroy {
templateData['user'] = p.name;
templateData['answer'] = validAnswerValue;
templateData['user-genitive'] = p.properties.genitive;
this.voiceService.playAudio(getAudioPathWithTemplate(template, '', templateData));
if(this.participants.length === 1) {
this.voiceService.playAudio(getAudioPathWithTemplate(template, '', templateData));
}
//this.voiceService.playAudio(getAudioPathWithTemplate(template, '', templateData));
this.voiceService.audioEndedSubject.pipe(takeUntil(this.destroyed$), take(1)).subscribe(r => {
if (note && validAnswer) {
this.voiceService.playAudio(getAudioPath(note))
@ -101,12 +105,14 @@ export class AnswerNotificationComponent implements OnInit, OnDestroy {
}
countdownCompleted() {
this.participants = [];
console.log(`countdown-completed`);
this.showCountdown = false;
this.isShown = false;
this.announceAudio = false;
this.countdown = 10;
this.apiService.continueGame().subscribe(r => console.log(r));
// this.apiService.continueGame().subscribe(r => console.log(r));
}
ngOnInit(): void {

View file

@ -1,12 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { EventService } from "../../services/event.service";
import { filter, map } from "rxjs/operators";
import { EventCardPlayed } from "../../../types/server-event";
import { ApiService } from "../../services/api.service";
import { animate, style, transition, trigger } from "@angular/animations";
import { API_URL } from "../../../app.constants";
import { getAudioPath } from "../../helper/tts.helper";
import { VoiceService } from "../../services/voice.service";
import {environment} from "../../../environments/environment";
@Component({
selector: 'app-card-played',
@ -59,7 +58,7 @@ export class CardPlayedComponent implements OnInit {
})
}
getImageUrl() {
return `${API_URL}/guests/photo/${this.participantId}?$t=${this.imgTimestamp}`;
return `${environment.API_URL}/guests/photo/${this.participantId}?$t=${this.imgTimestamp}`;
}
getAudioSrc(text: string) {

View file

@ -9,7 +9,7 @@ import { getAudioPath } from "../../helper/tts.helper";
})
export class GamePauseComponent implements OnInit, OnDestroy {
tstamp = new Date().getTime();
private interval: number;
private interval: NodeJS.Timeout;
constructor(private voiceService: VoiceService) { }

View file

@ -59,3 +59,6 @@
</div>
</div>
</div>
<div class="versus-container" *ngIf="action && action.type === gameQueueTypes.versus">
</div>

View file

@ -50,4 +50,11 @@ h1,h3 {
animation: results 3s 1;
background-color: $thg_yellow;
color: black;
}
.versus-container {
position: absolute;
width: 100%;
height: 100vh;
background-color: $thg_red;
}

View file

@ -3,7 +3,7 @@ import { EventGameQueue, QueueTypes } from "../../../types/server-event";
import { Participant } from "../../../types/participant";
import { ApiService } from "../../services/api.service";
import { Subject } from "rxjs";
import {map, takeUntil} from "rxjs/operators";
import {map, take, takeUntil} from "rxjs/operators";
import { Question } from "../../../types/question";
import { getAudioPath } from "../../helper/tts.helper";
import { PrizeDto } from "../../../types/prize.dto";

View file

@ -1,5 +1,5 @@
<div class="card rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'shadow': shadow, 'transparent': transparent, 'banned': banned, 'animate__flipInY': small }">
<figure class="p-1" *ngIf="participant">
<div *ngIf="participant" class="card rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'shadow': shadow, 'transparent': transparent, 'banned': banned, 'animate__flipInY': small }">
<figure class="p-1">
<img [src]="getImageUrl()" class="participant-photo img-fluid">
</figure>
<div class="card-title">

View file

@ -13,6 +13,7 @@
.transparent {
background: inherit;
max-width: 200px;
}
figure {
@ -66,7 +67,7 @@ figure {
}
.big {
font-size: 7em;
font-size: 3em;
color: $thg_green;
transition-delay: 2s;

View file

@ -4,9 +4,9 @@ import { EventService } from "../../services/event.service";
import { Observable, Subject, Subscription } from "rxjs";
import { filter, map, takeUntil } from "rxjs/operators";
import { EventCardPlayed, EventCardsChanged, EventPhotosUpdated, ServerEvent } from "../../../types/server-event";
import { API_URL } from "../../../app.constants";
import { ApiService } from "../../services/api.service";
import { CardItem } from "../../../types/card-item";
import {environment} from "../../../environments/environment";
@Component({
selector: 'app-participant-item',
@ -74,7 +74,7 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges {
getImageUrl() {
if(this.participant) {
return `${API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`;
return `${environment.API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`;
}
return null;
}

View file

@ -1,9 +1,10 @@
import { API_URL } from "../../app.constants";
import {environment} from "../../environments/environment";
export function getAudioPath(text: string, voice: number = 1) {
return `${API_URL}/voice/tts?text=${text}&voice=${voice}`;
return `${environment.API_URL}/voice/tts?text=${text}&voice=${voice}`;
}
export function getAudioPathWithTemplate(path: string, text: string, vars: { [index: string]: string }) {
const t = new Date().getTime();
return `${API_URL}/voice/${path}?text=${text}&vars=${JSON.stringify(vars)}&t=${t}`;
return `${environment.API_URL}/voice/${path}?text=${text}&vars=${JSON.stringify(vars)}&t=${t}`;
}

View file

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { API_URL } from "../../app.constants";
import { AppState } from "../../types/app-state";
import { Participant } from "../../types/participant";
import { Question } from "../../types/question";
@ -12,6 +11,7 @@ import { PrizeDto } from "../../types/prize.dto";
import {QuestionresultsDto} from "../../types/questionresults.dto";
import {map} from "rxjs/operators";
import {VersusItem} from "../../types/versus-item";
import {environment} from "../../environments/environment";
export class FeatureFlagStateDto {
name: string;
@ -57,83 +57,90 @@ export interface EndgameResultsDto {
})
export class ApiService {
constructor(private httpClient: HttpClient) { }
constructor(private httpClient: HttpClient) {
console.log(environment.API_URL);
}
public getAppState(state: string): Observable<AppState> {
return this.httpClient.get<AppState>(`${API_URL}/state/${state}`);
return this.httpClient.get<AppState>(`${environment.API_URL}/state/${state}`);
}
public getParticipants(): Observable<Participant[]> {
return this.httpClient.get<Participant[]>(`${API_URL}/guests`);
return this.httpClient.get<Participant[]>(`${environment.API_URL}/guests`);
}
public getParticipant(id: number): Observable<Participant> {
return this.httpClient.get<Participant>(`${API_URL}/guests/${id}`);
return this.httpClient.get<Participant>(`${environment.API_URL}/guests/${id}`);
}
public getQuestion(): Observable<Question> {
return this.httpClient.get<Question>(`${API_URL}/quiz`);
return this.httpClient.get<Question>(`${environment.API_URL}/quiz`);
}
public setAppState(state: string, value: string) {
return this.httpClient.post<AppState>(`${API_URL}/state`, {
return this.httpClient.post<AppState>(`${environment.API_URL}/state`, {
state,
value
});
}
getCards(telegramId: number): Observable<CardItem[]> {
return this.httpClient.get<CardItem[]>(`${API_URL}/cards/${telegramId}`);
return this.httpClient.get<CardItem[]>(`${environment.API_URL}/cards/${telegramId}`);
}
continueGame() {
console.log(`continue game`);
return this.httpClient.post(`${API_URL}/quiz/proceed`, {});
return this.httpClient.post(`${environment.API_URL}/quiz/proceed`, {});
}
questionTimeout() {
console.log(`continue game`);
return this.httpClient.post(`${environment.API_URL}/quiz/timeout`, {});
}
markQueueAsCompleted(_id: string) {
return this.httpClient.post(`${API_URL}/game/${_id}/complete`, {});
return this.httpClient.post(`${environment.API_URL}/game/${_id}/complete`, {});
}
pauseGame() {
return this.httpClient.post(`${API_URL}/game/pause`, {});
return this.httpClient.post(`${environment.API_URL}/game/pause`, {});
}
resumeGame() {
return this.httpClient.post(`${API_URL}/game/resume`, {});
return this.httpClient.post(`${environment.API_URL}/game/resume`, {});
}
getGameState() {
return this.httpClient.get<GameState>(`${API_URL}/game/state`);
return this.httpClient.get<GameState>(`${environment.API_URL}/game/state`);
}
getPenalty() {
console.log(`get penalty`);
return this.httpClient.get<PenaltyDto>(`${API_URL}/penalty`);
return this.httpClient.get<PenaltyDto>(`${environment.API_URL}/penalty`);
}
playExtraCards() {
console.log(`play extra cards`);
return this.httpClient.get(`${API_URL}/game/playextracards`);
return this.httpClient.get(`${environment.API_URL}/game/playextracards`);
}
getAdditionalQuestion(target: number) {
return this.httpClient.post<Question>(`${API_URL}/quiz/extraquestion`, {
return this.httpClient.post<Question>(`${environment.API_URL}/quiz/extraquestion`, {
telegramId: target,
});
}
getImageUrl(id: number) {
const timestamp = new Date().getTime();
return `${API_URL}/guests/photo/${id}?$t=${timestamp}}`;
return `${environment.API_URL}/guests/photo/${id}?$t=${timestamp}}`;
}
getPrize(): Observable<PrizeDto> {
return this.httpClient.get<PrizeDto>(`${API_URL}/gifts`);
return this.httpClient.get<PrizeDto>(`${environment.API_URL}/gifts`);
}
getQuestionResults() {
return this.httpClient.get<QuestionresultsDto[]>(`${API_URL}/quiz/question-results`).pipe(map((data) =>
return this.httpClient.get<QuestionresultsDto[]>(`${environment.API_URL}/quiz/question-results`).pipe(map((data) =>
data.map((item) => {
return {
...item,
@ -144,29 +151,29 @@ export class ApiService {
}
getFeatureFlagState(feature: string) {
return this.httpClient.get<FeatureFlagStateDto>(`${API_URL}/featureflag/${feature}`);
return this.httpClient.get<FeatureFlagStateDto>(`${environment.API_URL}/featureflag/${feature}`);
}
setFeatureFlagState(feature: string, state: boolean) {
return this.httpClient.post<FeatureFlagStateDto>(`${API_URL}/featureflag`, { name: feature, state: state });
return this.httpClient.post<FeatureFlagStateDto>(`${environment.API_URL}/featureflag`, { name: feature, state: state });
}
getStateDetails() {
return this.httpClient.get<ConfigRecordDto>(`${API_URL}/game/state-details`);
return this.httpClient.get<ConfigRecordDto>(`${environment.API_URL}/game/state-details`);
}
getVersus() {
return this.httpClient.get<VersusItem>(`${API_URL}/versus`);
return this.httpClient.get<VersusItem>(`${environment.API_URL}/versus`);
}
completeVersus(winner: number, loser: number) {
return this.httpClient.post(`${API_URL}/versus/complete`, {
return this.httpClient.post(`${environment.API_URL}/versus/complete`, {
winner: winner,
loser: loser
});
}
getEndgameResults() {
return this.httpClient.get<EndgameResultsDto>(`${API_URL}/quiz/endgame-results`)
return this.httpClient.get<EndgameResultsDto>(`${environment.API_URL}/quiz/endgame-results`)
}
}

View file

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {API_URL} from "../../app.constants";
import {environment} from "../../environments/environment";
@Injectable({
providedIn: 'root'
@ -10,22 +10,26 @@ export class TestingApiService {
constructor(private httpClient: HttpClient) { }
public simulateVersus() {
return this.httpClient.post(`${API_URL}/versus/simulate-versus`, {});
return this.httpClient.post(`${environment.API_URL}/versus/simulate-versus`, {});
}
resetAllVersusTasksAsIncompleted() {
return this.httpClient.post(`${API_URL}/versus/reset-all`, {});
return this.httpClient.post(`${environment.API_URL}/versus/reset-all`, {});
}
resetAllPlayersScore() {
return this.httpClient.post(`${API_URL}/guests/reset-score`, {});
return this.httpClient.post(`${environment.API_URL}/guests/reset-score`, {});
}
clearGameQueue() {
return this.httpClient.post(`${API_URL}/game/clear-queue`, {});
return this.httpClient.post(`${environment.API_URL}/game/clear-queue`, {});
}
simulateEndGamePoints() {
return this.httpClient.post(`${API_URL}/quiz/calculate-endgame-extrapoints`, {})
return this.httpClient.post(`${environment.API_URL}/quiz/calculate-endgame-extrapoints`, {})
}
simulateValidAnswer() {
return this.httpClient.post(`${environment.API_URL}/game/simulate-valid-answer`, {});
}
}

View file

@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
import { API_URL } from "../../app.constants";
import {delay, delayWhen, interval, Observable, of, Subject} from "rxjs";
import {ApiService} from "./api.service";
import {takeUntil, tap} from "rxjs/operators";
import {SharedMethods} from "../shared/sharedmethods";
import {environment} from "../../environments/environment";
@Injectable({
providedIn: 'root'
@ -45,11 +45,11 @@ export class VoiceService {
}
getAudioUrl(text: string,voice: number = 1) {
return `${API_URL}/voice/tts?voice=${voice}&text=${text}`
return `${environment.API_URL}/voice/tts?voice=${voice}&text=${text}`
}
getAudioUrlSSML(text: string) {
return `${API_URL}/voice/ssml?text=${encodeURI(text)}`
return `${environment.API_URL}/voice/ssml?text=${encodeURI(text)}`
}
audioEnded() {

View file

@ -15,6 +15,6 @@
</div>
<div *ngIf="step === 3" [@enterAnimation]>
<div class="video-container">
<video src="../../../assets/captions.mp4" autoplay></video>
<video src="../../../assets/finalcaption.mov" autoplay></video>
</div>
</div>

View file

@ -22,11 +22,11 @@
<p class="card-text">Если тебе требуется пояснять эту карточку - то игра не для тебя, налей себе алкоголь и побольше.</p>
</div>
</div>
<div class="card text-white bg-secondary mb-3" style="max-width: 18rem;" #shitCard>
<img src="../../../assets/cards/ShitCard.png" class="card-img-top" alt="...">
<div class="card text-white bg-secondary mb-3" style="max-width: 18rem;" #versusCard>
<img src="../../../assets/cards/VersusCard.png" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Говнокарта</h5>
<p class="card-text">Можно подкинуть еще один вопрос игроку, который правильно ответил.</p>
<h5 class="card-title">Поединок</h5>
<p class="card-text">Можно вызвать другого игрока на схватку 1 на 1 в мини-игре</p>
</div>
</div>
<div class="card text-white bg-success mb-3" style="max-width: 18rem;" #luckyCard>
@ -54,7 +54,7 @@
<img src="../../../assets/cards/BanPlayer.png" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Заблокировать игрока</h5>
<p class="card-text">Запрещает игроку давать ответы в следующих двух раундах</p>
<p class="card-text">Запрещает игроку давать ответы в случайном количестве раундов</p>
</div>
</div>
</div>

View file

@ -33,7 +33,7 @@ interface RuleItem {
export class OnboardingComponent implements OnInit, OnDestroy {
@ViewChild('avoidPenaltyCard') private avoidPenaltyCardEl: ElementRef;
@ViewChild('stolePrizeCard') private stolePrizeCardEl: ElementRef;
@ViewChild('shitCard') private shitCardEl: ElementRef;
@ViewChild('versusCard') private versusCardEl: ElementRef;
@ViewChild('luckyCard') private luckyCardEl: ElementRef;
@ViewChild('banPlayerCard') private banPlayerEl: ElementRef;
@ViewChild('doubleTreasureCard') private doubleTreasureCardEl: ElementRef;
@ -42,30 +42,26 @@ export class OnboardingComponent implements OnInit, OnDestroy {
{ text: 'Вопросы и ответы будут отображаться на экране и в Боте Благодарения.' },
{ text: 'Каждый игрок в начале игры имеет на руках 4 карты, набор карт определяется случайно. Описание карт ты найдешь ниже. После использования карты ты получаешь новую случайную карту.' },
{ text: 'На разыгрывание карты время ограничено, примерно 10 секунд.' },
{ text: 'Задача игрока - ответить правильно и быстрее других.' },
{ text: 'Первый игрок, ответивший правильно, получает одно очко и шанс выиграть приз.' },
{ text: 'Приз??? Какой приз?', screpa: true, voice: 2 },
{ text: 'Я не думаю, что их мозгов хватит для получения призов', screpa: true, voice: 2 },
{ text: 'А ты вообще кто такая?', hideWithoutVoice: true },
{ text: 'Ах, да.. простите, забыла представиться, я - Скрепа по фамилии Духовная', screpa: true, voice: 2},
{ text: 'И на кой ты нам нужна?', hideWithoutVoice: true },
{ text: 'Я тут, чтобы нарушать ход игры, и вообще тебя не спрашивали. ', screpa: true, voice: 2},
{ text: '[Ладно, ]В общем - чем больше правильных ответов - тем больше призов '},
{ text: 'Первый игрок, ответивший неправильно, получает наказание, и мы переходим к следующему вопросу' },
{ text: 'Вы долго просили оптимизировать геймплей для медленных и глупых, и мы это сделали!'},
{ text: 'Задача игрока - ответить правильно и быстрее других, ну или хотя бы просто правильно в течение 20 секунд' },
{ text: 'Первый игрок, ответивший правильно, получает два очка' },
{ text: 'Все остальные, ответившие правильно, получают одно очко'},
{ text: 'Иногда за неправильные ответы игроки будут получать наказания' },
{ text: 'Некоторые карты можно разыграть в любой момент, для этого нужно выбрать соответсвующую команду в боте'},
{ text: 'Избежать наказания можно только с помощью соотвествуещей карты, данную карту ты можешь сыграть перед озвучиванием наказания', action: () => {
this.shakeCard(this.avoidPenaltyCardEl);
}},
{ text: 'Карту "украсть приз" ты можешь сыграть в момент, когда кто-то собирается получить награду, но до момента того, как ты узнаешь, что это именно за приз', action: () => {
this.shakeCard(this.stolePrizeCardEl);
}},
{ text: '"Говно-карту" ты можешь разыграть в момент, когда кто-то ответил правильно, тем самым ты заставишь именно этого игрока ответить на один дополнительный вопрос. На одного игрока можно сыграть неограниченное количество этих карт', action: () => {
this.shakeCard(this.shitCardEl);
{ text: 'Карту "Поединок" ты можешь разыграть в любой момент, чтобы вызвать игрока на дуэль', action: () => {
this.shakeCard(this.versusCardEl);
}},
{ text: '"Лаки карту" ты сможешь сыграть после своего правильного ответа, она увеличит твои шансы на получение приза', action: () => {
this.shakeCard(this.luckyCardEl);
}},
{
text: 'Карту бана можно сыграть на любого игрока (даже себя), чтобы отправить его на пару ходов во Владикавказ. Игрок не сможет участвовать в игре в течение двух раундов',
text: 'Карту бана можно сыграть на любого игрока (даже себя), чтобы отправить его на пару ходов во Владикавказ. Игрок не сможет участвовать в случайном количестве раундов',
action: () => {
this.shakeCard(this.banPlayerEl);
}
@ -78,22 +74,10 @@ export class OnboardingComponent implements OnInit, OnDestroy {
},
{ text: 'Не торопись с ответами, игра идет до той поры, пока мы не разыграем все призы' },
{
text: 'Ах да, так как создатель игры, работает на Microsoft, мы добавили привычные вам баги, сейчас расскажу',
screpa: true,
voice: 2
text: 'Ах да, так как создатель игры, работает на Microsoft, мы добавили привычные вам баги, но все их оставим в секрете',
},
{
text: 'Если у вас нет вариантов ответа - перезайдите в бот или перезапустите телеграмм',
screpa: true,
voice: 2,
},
{
text: 'Остальные баги будут сюрпризом! Но не забывайте громко кричать, когда что-то работает не так',
screpa: true,
voice: 2,
},
{
text: 'Кажется правила закончились'
text: 'Кажется, правила закончились'
}
];
@ -172,7 +156,7 @@ export class OnboardingComponent implements OnInit, OnDestroy {
this.allRulesAnnounced = true;
this.voiceService.playAudio(getAudioPath(`Это все правила, надеюсь, все понятно. А если нет - сейчас Кирилл и Оксана вам все пояснят,
ну и совсем для тупых - пустила по кругу правила на экране,
а если ты их не поймешь - то за Путина голосовать пойдешь (или за Грузинскую мечту) . Каждый правильный ответ отнимает у Путина год жизни, постарайтесь!`));
а если ты их не поймешь - то очень жаль тебя глупенького`));
this.voiceService.audioEndedSubject.pipe(takeUntil(this.destroyed$),take(1)).subscribe(() => {
setInterval(() => { this.playNextRule() }, 6000);
this.currentRulePosition = 0

View file

@ -17,15 +17,16 @@ export class RegisterComponent implements OnInit {
playWelcome() {
const url = this.voiceService.getAudioUrlSSML(`<speak>
Гамарджоба, дорогие друзья! Спасибо, что вы в этот вечер с нами!<break time="1s"/>
Кто-то меня уже помнит, а для новеньких представлюсь: меня зовут Нино Алкошвили, и сегодня я буду вести эту игру.
<break time="1s"/>вай-мээ!
Хочу предупредить, что я уже бахнула вина, поэтому за юмор простите.
Кто-то меня уже помнит, а для новеньких представлюсь: меня зовут Нино Алкошвили, и вот уже в седьмой раз я буду вести эту игру.
Настало время зарегистрироваться! <break time="1s" />
Доставай свой телефон <break time="1s" /> и, я надеюсь, у тебя есть телеграм?<break time="1s" /><break time="2s" />
Достал?<break time="1s" />
Тогда сканируй кью ар код с индюшкой и проходи простую регистрацию, надеюсь, твоих мозгов на это хватит? ха-ха<break time="1s"/>
Для новоприбывших расскажу как тут все работает, мы играем в игру, используем для этого глаза направленные в телевизор и пальцы гладящие сенсорные поверности
ваших смарт устройств с установленным приложением Телеграм
Чтобы начать тебе надо отсканировать кью ар код с индюшкой и понажимать там несколько кнопочек.
<break time="2s" />
Кстати, кто проиграет - будет мыть посуду, а кто умненький - заберет много призов!
<break time="2s" />
Мы тут считаем, что хороший юмор это залог успешной жизни, но иногда можем выходить за рамки - поэтому просто простите,
если вы обиделись, рекомендуем вам пересмотреть взгляды на свою жизнь если вас такие пустяки задевают.
</speak>`);
this.voiceService.playAudio(url)

BIN
src/assets/finalcaption.mov Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View file

@ -1,3 +1,5 @@
export const environment = {
production: true
production: true,
API_URL: "https://thanksgiving2024.ngweb.io/api",
WEBSOCK_URL: "https://thanksgiving2024.ngweb.io"
};

View file

@ -3,7 +3,9 @@
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
production: false,
API_URL: "http://localhost:3000",
WEBSOCK_URL: "http://localhost:3000"
};
/*

View file

@ -7,6 +7,7 @@ export enum QueueTypes {
playExtraCard = 'play_extra_card',
screpa = 'screpa',
showresults = 'show_results',
versus = 'versus',
}
export interface EventPhotosUpdated {

View file

@ -3,7 +3,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
"types": ["node"]
},
"files": [
"src/main.ts",