diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 464c8dda..9125eed9 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -106,6 +106,11 @@ const routes: Routes = [ loadComponent: () => import("./overlays/map-breakdown/map-breakdown").then((m) => m.MapBreakdown), }, + { + path: "toast", + loadComponent: () => + import("./components/shopping/toast/toast-component").then((m) => m.ToastComponent), + }, ]; @NgModule({ diff --git a/src/app/components/shopping/toast/toast-component.css b/src/app/components/shopping/toast/toast-component.css new file mode 100644 index 00000000..c42b578f --- /dev/null +++ b/src/app/components/shopping/toast/toast-component.css @@ -0,0 +1,36 @@ +@keyframes swish { + from { + clip-path: inset(0 100% 0 0); + } + to { + clip-path: inset(0 0 0 0); + } +} + +.animate-swish { + animation: swish 300ms ease-in-out forwards; + will-change: clip-path, width; +} + +.animate-swish-out { + animation: swish 300ms ease-in-out reverse forwards; + will-change: clip-path, width; +} + +.bg-fade-defender { + background: linear-gradient( + to right, + rgba(var(--defender-color-rgb), 0.5) 10%, + rgba(var(--defender-color-rgb), 0.25) 40%, + transparent + ); +} + +.bg-fade-attacker { + background: linear-gradient( + to right, + rgba(var(--attacker-color-rgb), 0.5) 10%, + rgba(var(--attacker-color-rgb), 0.25) 40%, + transparent + ); +} diff --git a/src/app/components/shopping/toast/toast-component.html b/src/app/components/shopping/toast/toast-component.html new file mode 100644 index 00000000..f5d40b64 --- /dev/null +++ b/src/app/components/shopping/toast/toast-component.html @@ -0,0 +1,55 @@ +@if(this.dataModel.match().roundPhase === "shopping") { +
+
+
+
+
+ @if (toastInfo().selectedTeam !== "none") { +
+ +
+ } @else { +
+ } +
+ @if(toastInfo().title !== "") { + + {{ toastInfo().title }} + + } + + {{ toastInfo().message }} + +
+ @if (toastInfo().eventLogoEnabled) { +
+ +
+ } @else { +
+ } +
+
+
+
+
+} diff --git a/src/app/components/shopping/toast/toast-component.ts b/src/app/components/shopping/toast/toast-component.ts new file mode 100644 index 00000000..dcb275ec --- /dev/null +++ b/src/app/components/shopping/toast/toast-component.ts @@ -0,0 +1,63 @@ +import { Component, computed, effect, inject, signal } from "@angular/core"; +import { DataModelService } from "../../../services/dataModel.service"; + +@Component({ + selector: "app-toast", + imports: [], + templateUrl: "./toast-component.html", + styleUrl: "./toast-component.css", +}) +export class ToastComponent { + dataModel = inject(DataModelService); + + toastInfo = signal(Object.assign({}, this.dataModel.toastInfo())); + + shouldHide = computed( + () => !this.dataModel.toastInfo().active || !(this.dataModel.match().roundPhase === "shopping"), + ); + + teamSite = computed(() => { + if (this.dataModel.match().teams[this.toastInfo().selectedTeam == "left" ? 0 : 1].isAttacking) { + return "attacker"; + } else { + return "defender"; + } + }); + + tournamentIconUrl = computed(() => { + const logo = this.dataModel.tournamentInfo().logoUrl; + if (logo && logo !== "") return logo; + else return "assets/misc/logo.webp"; + }); + + hide = true; + inAnimation = false; + outAnimation = false; + + hideAnimationEffect = effect(() => { + if (this.shouldHide()) { + this.outAnimation = true; + setTimeout(() => { + this.outAnimation = false; + this.hide = true; + }, 300); + } else { + this.outAnimation = false; + this.hide = false; + this.inAnimation = true; + setTimeout(() => { + this.inAnimation = false; + }, 300); + } + }); + + delayEndToastEffect = effect(() => { + if (this.shouldHide()) { + setTimeout(() => { + this.toastInfo.set(Object.assign({}, this.dataModel.toastInfo())); + }, 350); + } else { + this.toastInfo.set(Object.assign({}, this.dataModel.toastInfo())); + } + }); +} diff --git a/src/app/overlays/match-overlay/match-overlay.component.html b/src/app/overlays/match-overlay/match-overlay.component.html index 28f97d8a..f37bdb56 100644 --- a/src/app/overlays/match-overlay/match-overlay.component.html +++ b/src/app/overlays/match-overlay/match-overlay.component.html @@ -20,3 +20,5 @@ + + diff --git a/src/app/overlays/match-overlay/match-overlay.component.ts b/src/app/overlays/match-overlay/match-overlay.component.ts index b035faa1..01ff7481 100644 --- a/src/app/overlays/match-overlay/match-overlay.component.ts +++ b/src/app/overlays/match-overlay/match-overlay.component.ts @@ -15,6 +15,7 @@ import { EndroundBannerComponent } from "../../components/combat/endround-banner import { TimeoutComponent } from "../../components/common/timeout/timeout.component"; import { TopBackgroundComponent } from "../../components/top/match/background/background.component"; import { OneVersusOneComponent } from "../../components/combat/1v1/1v1.component"; +import { ToastComponent } from "../../components/shopping/toast/toast-component"; @Component({ selector: "app-match-overlay", @@ -34,6 +35,7 @@ import { OneVersusOneComponent } from "../../components/combat/1v1/1v1.component TimeoutComponent, TopBackgroundComponent, OneVersusOneComponent, + ToastComponent, ], templateUrl: "./match-overlay.component.html", styleUrl: "./match-overlay.component.css", diff --git a/src/app/overlays/testing-agent-select/testing-agent-select.component.ts b/src/app/overlays/testing-agent-select/testing-agent-select.component.ts index c2ef93ae..2fc2756a 100644 --- a/src/app/overlays/testing-agent-select/testing-agent-select.component.ts +++ b/src/app/overlays/testing-agent-select/testing-agent-select.component.ts @@ -90,6 +90,14 @@ export class TestingAgentSelectComponent implements OnInit { sponsors: [], }, }, + toastInfo: { + active: false, + duration: 10000, + title: "", + message: "", + selectedTeam: "none", + eventLogoEnabled: false, + }, timeoutState: { techPause: false, leftTeam: false, diff --git a/src/app/overlays/testing/testing.component.html b/src/app/overlays/testing/testing.component.html index ca40412a..76e93a59 100644 --- a/src/app/overlays/testing/testing.component.html +++ b/src/app/overlays/testing/testing.component.html @@ -17,6 +17,7 @@ + diff --git a/src/app/overlays/testing/testing.component.ts b/src/app/overlays/testing/testing.component.ts index 8155ecc6..7a4b202e 100644 --- a/src/app/overlays/testing/testing.component.ts +++ b/src/app/overlays/testing/testing.component.ts @@ -11,6 +11,7 @@ import { IMatchData } from "../../services/Types"; }) export class TestingComponent implements OnInit { dataModel = inject(DataModelService); + private toastTimerRef?: ReturnType; match: IMatchData = initialMatchData; bgCounter = 1; @@ -99,6 +100,14 @@ export class TestingComponent implements OnInit { sponsors: [], }, }, + toastInfo: { + active: false, + duration: 10000, + title: "", + message: "", + selectedTeam: "none", + eventLogoEnabled: false, + }, teams: [ { players: [ @@ -815,4 +824,45 @@ export class TestingComponent implements OnInit { return ret; }); } + + showToast() { + const currentlyActive = this.dataModel.match().toastInfo.active; + + if (currentlyActive) { + // Deactivate immediately (mimics pressing the hotkey again) + clearTimeout(this.toastTimerRef); + this.toastTimerRef = undefined; + this.dataModel.match.update((v) => { + const ret = v; + ret.toastInfo.active = false; + return ret; + }); + } else { + // Activate + this.dataModel.match.update((v) => { + const ret = v; + ret.toastInfo.active = true; + ret.toastInfo.title = "Spezial Spectra Developer Announcement"; + ret.toastInfo.message = + "This is a live toast preview with the biggest changes. Thanks for using Spectra!"; + ret.toastInfo.selectedTeam = "left"; + ret.toastInfo.duration = null; + ret.toastInfo.eventLogoEnabled = true; + return ret; + }); + + const duration = this.dataModel.match().toastInfo.duration; + if (duration !== null) { + clearTimeout(this.toastTimerRef); + this.toastTimerRef = setTimeout(() => { + this.dataModel.match.update((v) => { + const ret = v; + ret.toastInfo.active = false; + return ret; + }); + this.toastTimerRef = undefined; + }, duration); + } + } + } } diff --git a/src/app/services/Types.ts b/src/app/services/Types.ts index 597a53e9..fec08596 100644 --- a/src/app/services/Types.ts +++ b/src/app/services/Types.ts @@ -14,6 +14,7 @@ export interface IMatchData { switchRound: number; firstOtRound: number; attackersWon: boolean; + toastInfo: IToastInfo; } export interface ITeamData { @@ -84,6 +85,15 @@ export interface ITimeoutState { timeRemaining: number; } +export interface IToastInfo { + active: boolean; + title: string; + message: string; + duration: number | null; + eventLogoEnabled: boolean; + selectedTeam: "left" | "right" | "none"; +} + //#endregion //#region Tools diff --git a/src/app/services/dataModel.service.ts b/src/app/services/dataModel.service.ts index 43150052..846ba673 100644 --- a/src/app/services/dataModel.service.ts +++ b/src/app/services/dataModel.service.ts @@ -106,6 +106,7 @@ export class DataModelService { public readonly sponsorInfo = computed(() => this.match().tools.sponsorInfo); public readonly watermarkInfo = computed(() => this.match().tools.watermarkInfo); public readonly tournamentInfo = computed(() => this.match().tools.tournamentInfo); + public toastInfo = computed(() => this.match().toastInfo, { equal: () => false }); public readonly playercamsInfo = computed(() => this.match().tools.playercamsInfo, { equal: () => false, }); @@ -195,6 +196,14 @@ export const initialMatchData: IMatchData = { sponsors: [], }, }, + toastInfo: { + active: false, + duration: 10000, + title: "", + message: "", + eventLogoEnabled: true, + selectedTeam: "none", + }, timeoutState: { techPause: false, leftTeam: false,