versus implementation

This commit is contained in:
Kirill Ivlev 2024-11-12 02:22:00 +04:00
parent 3bb63d1d5a
commit afabd52e02
23 changed files with 332 additions and 11 deletions

View file

@ -4,6 +4,7 @@ import { HomeComponent } from "./home/home.component";
import { Observable, of } from "rxjs"; import { Observable, of } from "rxjs";
import {ConfigurationComponent} from "./configuration/configuration.component"; import {ConfigurationComponent} from "./configuration/configuration.component";
import {AdminMainComponent} from "./admin-main/admin-main.component"; import {AdminMainComponent} from "./admin-main/admin-main.component";
import {AdminTestingComponent} from "./admin-testing/admin-testing.component";
export class AdminGuard { export class AdminGuard {
@ -20,7 +21,6 @@ export class AdminGuard {
} }
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
@ -36,7 +36,13 @@ const routes: Routes = [
path: 'configuration', path: 'configuration',
component: ConfigurationComponent, component: ConfigurationComponent,
canDeactivate: [AdminGuard], canDeactivate: [AdminGuard],
}] },
{
path:'testing',
component: AdminTestingComponent,
canDeactivate: [AdminGuard],
}
]
}, },
] ]

View file

@ -0,0 +1,12 @@
<div class="game-testing m-2" *ngIf="!prodMode">
<h3>Game testing menu</h3>
<button class="btn btn-danger" (click)="simulateVersus()">Begin versus</button>
<button class="btn btn-danger" disabled>Stop versus</button>
<button class="btn btn-danger" disabled>Simulate endgame points</button>
</div>
<div class="game-testing m-2" *ngIf="prodMode">
<div class="alert alert-danger">
You are in prod mode, testing disabled
</div>
</div>

View file

@ -0,0 +1,6 @@
div {
button {
margin: 5px;
}
}

View file

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminTestingComponent } from './admin-testing.component';
describe('AdminTestingComponent', () => {
let component: AdminTestingComponent;
let fixture: ComponentFixture<AdminTestingComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AdminTestingComponent]
});
fixture = TestBed.createComponent(AdminTestingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,40 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ApiService} from "../../services/api.service";
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
import {EventService} from "../../services/event.service";
import {TestingApiService} from "../../services/testing-api.service";
@Component({
selector: 'app-admin-testing',
templateUrl: './admin-testing.component.html',
styleUrls: ['./admin-testing.component.scss']
})
export class AdminTestingComponent implements OnInit, OnDestroy {
prodMode = false;
destroyed$ = new Subject<void>();
constructor(
private apiService: ApiService,
private eventService: EventService,
private testingApiService: TestingApiService) {
}
ngOnInit(): void {
this.getFFState();
this.eventService.featureFlagChanged.pipe(takeUntil(this.destroyed$)).subscribe((r) => this.getFFState());
}
private getFFState() {
this.apiService.getFeatureFlagState("ProdMode").pipe(takeUntil(this.destroyed$)).subscribe((res) =>
{
this.prodMode = res.state;
});
}
ngOnDestroy() {
this.destroyed$.complete();
}
simulateVersus() {
this.testingApiService.simulateVersus().pipe(takeUntil(this.destroyed$)).subscribe((r) => console.log(r));
}
}

View file

@ -9,6 +9,7 @@ import { ConfigurationComponent } from './configuration/configuration.component'
import { AdminNavComponent } from './components/admin-nav/admin-nav.component'; import { AdminNavComponent } from './components/admin-nav/admin-nav.component';
import { AdminMainComponent } from './admin-main/admin-main.component'; import { AdminMainComponent } from './admin-main/admin-main.component';
import { FeatureflagsComponent } from './components/featureflags/featureflags.component'; import { FeatureflagsComponent } from './components/featureflags/featureflags.component';
import { AdminTestingComponent } from './admin-testing/admin-testing.component';
@ -21,6 +22,7 @@ import { FeatureflagsComponent } from './components/featureflags/featureflags.co
AdminNavComponent, AdminNavComponent,
AdminMainComponent, AdminMainComponent,
FeatureflagsComponent, FeatureflagsComponent,
AdminTestingComponent,
], ],
imports: [ imports: [
CommonModule, AdminRoutingModule, SharedModule, CommonModule, AdminRoutingModule, SharedModule,

View file

@ -1,3 +1,4 @@
<a routerLink="/admin/">Main</a> <a routerLink="/admin/">Main</a>
<a routerLink="/admin/testing">Testing</a>
<a routerLink="/admin/configuration">Config</a> <a routerLink="/admin/configuration">Config</a>

View file

@ -29,6 +29,15 @@ export class FeatureflagsComponent implements OnInit, OnDestroy {
this.apiService.getFeatureFlagState(featureFlag).pipe(takeUntil(this.destroyed$)).subscribe((result) => { this.apiService.getFeatureFlagState(featureFlag).pipe(takeUntil(this.destroyed$)).subscribe((result) => {
if(!this.features.find((x) => x.name === result.name)) { if(!this.features.find((x) => x.name === result.name)) {
this.features.push(result); this.features.push(result);
this.features.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
} else { } else {
const index = this.features.findIndex((x) => x.name === result.name); const index = this.features.findIndex((x) => x.name === result.name);
this.features[index] = result; this.features[index] = result;

View file

@ -1,5 +1,7 @@
<app-toast> <app-versus *ngIf="versusInProgress" [player1]="versusData.player1" [player2]="versusData.player2">
</app-versus>
<app-toast>
</app-toast> </app-toast>
<audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio> <audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View file

@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import { API_URL, WEBSOCK_URL } from '../app.constants'; import { API_URL, WEBSOCK_URL } from '../app.constants';
import { EventService } from "./services/event.service"; import { EventService } from "./services/event.service";
import { EventStateChanged, ServerEvent } from "../types/server-event"; import {EventStateChanged, ServerEvent, VersusBeginEvent} from "../types/server-event";
import { ApiService } from "./services/api.service"; import { ApiService } from "./services/api.service";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { filter, map, takeUntil } from "rxjs/operators"; import { filter, map, takeUntil } from "rxjs/operators";
@ -20,6 +20,8 @@ export class AppComponent implements OnInit, OnDestroy {
title = 'thanksgiving'; title = 'thanksgiving';
connection = io(WEBSOCK_URL, { transports: ['websocket']}); connection = io(WEBSOCK_URL, { transports: ['websocket']});
destroyed = new Subject<void>(); destroyed = new Subject<void>();
versusInProgress = false;
versusData: VersusBeginEvent;
audioSrc: string; audioSrc: string;
constructor( constructor(
@ -39,9 +41,11 @@ export class AppComponent implements OnInit, OnDestroy {
this.eventService.emit(data); this.eventService.emit(data);
}); });
this.apiService.getAppState('main').subscribe((result) => { this.apiService.getAppState('main').subscribe((result) => {
this.router.navigate([`/${result.value}`]).then(() => { if(this.router.url.indexOf('admin') === -1) {
console.log(`navigated to ${result.value}`); this.router.navigate([`/${result.value}`]).then(() => {
}) console.log(`navigated to ${result.value}`);
})
}
}); });
this.eventService.stateChangedEvent.pipe( this.eventService.stateChangedEvent.pipe(
map(e => e.data), map(e => e.data),
@ -55,6 +59,7 @@ export class AppComponent implements OnInit, OnDestroy {
console.log(text); console.log(text);
this.audioSrc = text; this.audioSrc = text;
}) })
this.setupVersusHandler();
} }
ngOnDestroy() { ngOnDestroy() {
this.destroyed.complete(); this.destroyed.complete();
@ -63,4 +68,14 @@ export class AppComponent implements OnInit, OnDestroy {
onAudioEnded() { onAudioEnded() {
this.voiceService.audioEnded(); this.voiceService.audioEnded();
} }
private setupVersusHandler() {
console.log(this.routeSnapshot.snapshot.url);
this.eventService.versusBegin.pipe(takeUntil(this.destroyed)).subscribe(r => {
if(this.router.url.indexOf('admin') === -1) {
this.versusInProgress = true;
this.versusData = r.data;
}
})
}
} }

View file

@ -27,6 +27,7 @@ import { AvatarComponent } from './components/avatar/avatar.component';
import { FinishComponent } from './views/finish/finish.component'; import { FinishComponent } from './views/finish/finish.component';
import { InitialComponent } from './views/initial/initial.component'; import { InitialComponent } from './views/initial/initial.component';
import { SkrepaComponent } from './components/skrepa/skrepa.component'; import { SkrepaComponent } from './components/skrepa/skrepa.component';
import { VersusComponent } from './components/versus/versus.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -50,6 +51,7 @@ import { SkrepaComponent } from './components/skrepa/skrepa.component';
FinishComponent, FinishComponent,
InitialComponent, InitialComponent,
SkrepaComponent, SkrepaComponent,
VersusComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View file

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

View file

@ -11,6 +11,10 @@
padding: 0px; padding: 0px;
} }
.transparent {
background: inherit;
}
figure { figure {
border-radius:100%; border-radius:100%;
display:inline-block; display:inline-block;

View file

@ -23,6 +23,8 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges {
imgTimestamp = (new Date()).getTime(); imgTimestamp = (new Date()).getTime();
addAnimatedClass = false; addAnimatedClass = false;
@Input() bannedRemaining: number|undefined = 0; @Input() bannedRemaining: number|undefined = 0;
@Input() transparent = false;
@Input() shadow = true;
constructor(private eventService: EventService, private apiService: ApiService) { constructor(private eventService: EventService, private apiService: ApiService) {
} }

View file

@ -0,0 +1,14 @@
<div class="versus">
<div class="d-flex players">
<div class="player-one">
<app-participant-item [participant]="player1data" [small]="true" [shadow]="false" [transparent]="true">
</app-participant-item>
</div>
<div class="player-two">
<app-participant-item [participant]="player2data" [small]="true" [shadow]="false" [transparent]="true">
</app-participant-item>
</div>
</div>
</div>

View file

@ -0,0 +1,86 @@
@import '../../../styles';
@keyframes slideDown {
0% {
top: -100vh;
}
100% {
top: 0;
}
}
@keyframes slideRight {
0% {
left: -100vh;
}
100% {
left: 0;
}
}
@keyframes slideLeft {
0% {
right: -100vh;
}
100% {
right: 0;
}
}
.versus {
background-color: $thg_brown;
z-index: 20000;
position: fixed;
top: -100vh;
left: 0;
width: 100%;
height: 100vh;
animation: slideDown 1s ease forwards;
}
.versus:before {
content: "VS";
position: absolute;
font-size: 20vw; /* Large size for the background */
color: rgba(255, 255, 255, 0.2); /* Light opacity */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
z-index: 22000; /* Puts it behind other content */
pointer-events: none;
}
.players {
height: 100vh;
}
/* Left player area */
.player-one {
position: relative;
width: 50%;
height: 100%;
background-color: #4a90e2;
clip-path: polygon(0 0, 100% 0, 50% 100%, 0 100%);
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 2rem;
font-weight: bold;
animation: slideRight 1s ease forwards;
}
/* Right player area */
.player-two {
position: relative;
width: 50%;
height: 100%;
background-color: #d9534f;
clip-path: polygon(50% 0, 100% 0, 100% 100%, 0 100%);
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 2rem;
font-weight: bold;
animation: slideLeft 1s ease forwards;
}

View file

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VersusComponent } from './versus.component';
describe('VersusComponent', () => {
let component: VersusComponent;
let fixture: ComponentFixture<VersusComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [VersusComponent]
});
fixture = TestBed.createComponent(VersusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,32 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {ApiService} from "../../services/api.service";
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
import {Participant} from "../../../types/participant";
@Component({
selector: 'app-versus',
templateUrl: './versus.component.html',
styleUrls: ['./versus.component.scss']
})
export class VersusComponent implements OnInit, OnDestroy{
@Input() player1: number;
@Input() player2: number;
player1data: Participant;
player2data: Participant;
destroyed$ = new Subject<void>();
constructor(private apiService: ApiService) {
}
ngOnInit() {
this.loadPlayersData();
}
ngOnDestroy() {
this.destroyed$.complete();
}
loadPlayersData() {
this.apiService.getParticipant(this.player1).pipe(takeUntil(this.destroyed$)).subscribe((r) => this.player1data = r);
this.apiService.getParticipant(this.player2).pipe(takeUntil(this.destroyed$)).subscribe((r) => this.player2data = r);
}
}

View file

@ -9,7 +9,7 @@ import {
EventUserAdded, EventUserAdded,
EventWrongAnswerReceived, EventWrongAnswerReceived,
QuestionChangedEvent, QuestionChangedEvent,
ServerEvent, UserPropertyChanged ServerEvent, UserPropertyChanged, VersusBeginEvent
} from "../../types/server-event"; } from "../../types/server-event";
@Injectable({ @Injectable({
@ -31,7 +31,8 @@ export class EventService {
public gameResumed = new EventEmitter<ServerEvent<void>>(); public gameResumed = new EventEmitter<ServerEvent<void>>();
public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>(); public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>();
public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>(); public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>();
public featureFlagChanged = new EventEmitter<ServerEvent<void>>(); public featureFlagChanged = new EventEmitter<ServerEvent<void>>()
public versusBegin = new EventEmitter<ServerEvent<VersusBeginEvent>>();
constructor() { } constructor() { }
public emit(event: ServerEvent<any>) { public emit(event: ServerEvent<any>) {
@ -85,6 +86,9 @@ export class EventService {
case "feature_flag_changed": case "feature_flag_changed":
this.featureFlagChanged.emit(event); this.featureFlagChanged.emit(event);
break; break;
case "begin_versus":
this.versusBegin.emit(event as ServerEvent<VersusBeginEvent>);
break;
} }
} }
} }

View file

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {API_URL} from "../../app.constants";
@Injectable({
providedIn: 'root'
})
export class TestingApiService {
constructor(private httpClient: HttpClient) { }
public simulateVersus() {
return this.httpClient.post(`${API_URL}/game/simulate-versus`, {});
}
}

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { TestingApiService } from './testing-api.service';
describe('TestingapiService', () => {
let service: TestingApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(TestingApiService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -1,3 +1,8 @@
export class FeatureFlagList { export class FeatureFlagList {
static readonly FeatureFlags: string[] = ["EnableEndgamePoints"]; static readonly FeatureFlags: string[] = [
"EnableEndgamePoints",
"DontMarkQuestionsAsCompleted",
"DisableVoice",
"ProdMode",
];
} }

View file

@ -51,6 +51,11 @@ export interface EventScoreChanged {
newScore: number; newScore: number;
} }
export interface VersusBeginEvent {
player1: number;
player2: number;
}
export interface EventGameQueue { export interface EventGameQueue {
text?: string; text?: string;
target: number; target: number;
@ -88,5 +93,6 @@ export interface ServerEvent<T> {
| 'notification' | 'notification'
| 'user_property_changed' | 'user_property_changed'
| 'feature_flag_changed' | 'feature_flag_changed'
| 'begin_versus'
data: T data: T
} }