diff --git a/app/app-services.ts b/app/app-services.ts index d6503e5dfbe6..41f9bd8a3cbb 100644 --- a/app/app-services.ts +++ b/app/app-services.ts @@ -115,7 +115,6 @@ export { SseService } from 'services/server-sent-events'; // WIDGETS export { WidgetSource, WidgetsService } from './services/widgets'; -export { StreamBossService } from 'services/widgets/settings/stream-boss'; export { MediaShareService } from 'services/widgets/settings/media-share'; export { AlertBoxService } from 'services/widgets/settings/alert-box'; export { SpinWheelService } from 'services/widgets/settings/spin-wheel'; diff --git a/app/components-react/widgets/StreamBoss.tsx b/app/components-react/widgets/StreamBoss.tsx new file mode 100644 index 000000000000..8c9035c098ca --- /dev/null +++ b/app/components-react/widgets/StreamBoss.tsx @@ -0,0 +1,334 @@ +import React, { useState } from 'react'; +import { Button, Menu, message } from 'antd'; +import { $t } from 'services/i18n'; +import { IWidgetCommonState, useWidget, WidgetModule } from './common/useWidget'; +import { WidgetLayout } from './common/WidgetLayout'; +import FormFactory, { TInputValue } from 'components-react/shared/inputs/FormFactory'; +import Form from 'components-react/shared/inputs/Form'; +import { IBaseMetadata, metadata } from '../shared/inputs/metadata'; +import { authorizedHeaders, jfetch } from 'util/requests'; +import { Services } from 'components-react/service-provider'; +import { assertIsDefined } from 'util/properties-type-guards'; +import { TPlatform } from 'services/platforms'; +import styles from './GenericGoal.m.less'; + +type TStreamBossMode = 'fixed' | 'incremental' | 'overkill'; + +interface IStreamBossGoal { + boss_img: string; + boss_name: string; + current_health: number; + mode: TStreamBossMode; + multiplier: 1; + percent: number; + total_health: number; +} + +type TStreamBossGoalSetting = keyof IStreamBossGoal; +type TStreamBossGoalMeta = PartialRec; + +function fromGoalMeta(meta: TStreamBossGoalMeta): Dictionary { + return meta as Dictionary; +} + +interface IStreamBossState extends IWidgetCommonState { + data: { + goal: IStreamBossGoal | null; + settings: { + background_color: string; + bar_bg_color: string; + bar_color: string; + bar_text_color: string; + bg_transparent: boolean; + bit_multiplier: number; + boss_heal: boolean; + donation_multiplier: boolean; + fade_time: number; + follow_multiplier: boolean; + font: string; + incr_amount: string; + kill_animation: string; + overkill_min: number; + overkill_multiplier: number; + skin: string; + sub_multiplier: number; + superchat_multiplier: number; + text_color: string; + }; + }; +} + +type TStreamBossSettings = IStreamBossState['data']['settings']; +type TStreamBossSetting = keyof TStreamBossSettings; +type TStreamBossMeta = PartialRec; + +function fromMeta(meta: TStreamBossMeta): Dictionary { + return meta as Dictionary; +} + +export function StreamBoss() { + const { + settings, + goalSettings, + bossMeta, + visualMeta, + multipliersMeta, + bossGoalMeta, + bossModeMeta, + hasLoadedSettings, + updateSetting, + setSelectedTab, + selectedTab, + saveGoal, + resetGoal, + } = useStreamBoss(); + + const hasGoal = !!goalSettings; + + const [bossCreateValues, setBossCreateValues] = useState< + Pick + >({ + total_health: 4800, + mode: 'fixed', + }); + + function updateBossCreate(key: string) { + return (val: TInputValue) => { + setBossCreateValues({ ...bossCreateValues, [key]: val }); + }; + } + + const mode = bossCreateValues.mode as TStreamBossMode; + + return ( + + setSelectedTab(e.key)} selectedKeys={[selectedTab]}> + {$t('Manage Battle')} + {$t('Boss Settings')} + {$t('Visual Settings')} + +
+ {hasLoadedSettings(settings) && + selectedTab === 'goal' && + (hasGoal ? ( + + ) : ( + <> + + + + + ))} + {hasLoadedSettings(settings) && selectedTab === 'general' && ( + <> + + + + )} + {hasLoadedSettings(settings) && selectedTab === 'visual' && ( + + )} + +
+ ); +} + +function BossDisplay(p: { goal: IStreamBossGoal; resetGoal: () => void }) { + return ( +
+
+ {$t('Current Boss Name')} + {p.goal.boss_name} +
+
+ {$t('Total Health')} + {p.goal.total_health} +
+
+ {$t('Current Health')} + {p.goal.current_health} +
+
+ {$t('Mode')} + {p.goal.mode} +
+ +
+ ); +} + +export class StreamBossModule extends WidgetModule { + get UserService() { + return Services.UserService; + } + + get goalSettings() { + return this.widgetData.goal; + } + + get bossGoalMeta() { + return fromGoalMeta({ + total_health: metadata.number({ + label: $t('Starting Health'), + required: true, + min: 0, + }), + mode: metadata.list({ + label: $t('Mode'), + options: [ + { + label: $t('Fixed'), + value: 'fixed', + }, + { + label: $t('Incremental'), + value: 'incremental', + }, + { + label: $t('Overkill'), + value: 'overkill', + }, + ], + }), + }); + } + + bossModeMeta(mode: TStreamBossMode) { + if (mode === 'incremental') { + return fromMeta({ incr_amount: metadata.number({ label: $t('Increment Amount') }) }); + } else if (mode === 'overkill') { + return fromMeta({ + overkill_multiplier: metadata.number({ label: $t('Overkill Multiplier') }), + overkill_min: metadata.number({ label: $t('Overkill Min Health') }), + }); + } else { + return fromMeta({}); + } + } + + get bossMeta() { + return fromMeta({ + fade_time: metadata.slider({ + label: $t('Fade Time (s)'), + min: 0, + max: 20, + }), + boss_heal: metadata.bool({ label: $t('Damage From Boss Heals') }), + skin: metadata.list({ + label: $t('Theme'), + options: [ + { label: 'Default', value: 'default' }, + { label: 'Future', value: 'future' }, + { label: 'No Image', value: 'noimg' }, + { label: 'Slim', value: 'pill' }, + { label: 'Curved', value: 'future-curve' }, + ], + }), + }); + } + + get multipliersMeta() { + const platform = this.UserService.views.platform?.type; + const platformMultipliers: PartialRec = { + twitch: { + bit_multiplier: metadata.number({ label: $t('Damage Per Bit') }), + sub_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + follow_multiplier: metadata.number({ label: $t('Damage Per Follower') }), + }, + facebook: { + follow_multiplier: metadata.number({ label: $t('Damage Per Follower') }), + sub_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + }, + youtube: { + sub_multiplier: metadata.number({ label: $t('Damage Per Membership') }), + superchat_multiplier: metadata.number({ label: $t('Damage Per Superchat Dollar') }), + follow_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + }, + trovo: { + sub_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + follow_multiplier: metadata.number({ label: $t('Damage Per Follower') }), + }, + }; + + return fromMeta({ + ...(platform && platformMultipliers[platform] ? platformMultipliers[platform] : {}), + donation_multiplier: metadata.number({ label: $t('Damage Per Dollar Donation') }), + }); + } + + get visualMeta() { + return fromMeta({ + kill_animation: metadata.animation({ label: $t('Kill Animation') }), + bg_transparent: metadata.bool({ label: $t('Transparent Background') }), + background_color: metadata.color({ label: $t('Background Color') }), + text_color: metadata.color({ label: $t('Text Color') }), + bar_text_color: metadata.color({ label: $t('Health Text Color') }), + bar_color: metadata.color({ label: $t('Health Bar Color') }), + bar_bg_color: metadata.color({ label: $t('Health Bar Background Color') }), + font: { type: 'fontFamily', label: $t('Font') }, + }); + } + + get headers() { + return authorizedHeaders( + Services.UserService.apiToken, + new Headers({ 'Content-Type': 'application/json' }), + ); + } + + resetGoal() { + const url = this.config.goalUrl; + if (!url) return; + jfetch(new Request(url, { method: 'DELETE', headers: this.headers })); + this.setGoalData(null); + } + + async saveGoal(options: Dictionary) { + const url = this.config.goalUrl; + if (!url) return; + try { + const resp: IStreamBossState['data'] = await jfetch( + new Request(url, { + method: 'POST', + headers: this.headers, + body: JSON.stringify(options), + }), + ); + this.setGoalData(resp.goal); + } catch (e: unknown) { + message.error({ content: (e as any).result.message, duration: 2 }); + } + } + + private setGoalData(goal: IStreamBossGoal | null) { + assertIsDefined(this.state.widgetData.data); + this.state.mutate(state => { + state.widgetData.data.goal = goal; + }); + } + + patchAfterFetch(data: IStreamBossState['data']): IStreamBossState['data'] { + if (Array.isArray(data.goal)) data.goal = null; + return data; + } +} + +function useStreamBoss() { + return useWidget(); +} diff --git a/app/components-react/widgets/common/WidgetWindow.tsx b/app/components-react/widgets/common/WidgetWindow.tsx index e82407b707d6..fe286088b8d6 100644 --- a/app/components-react/widgets/common/WidgetWindow.tsx +++ b/app/components-react/widgets/common/WidgetWindow.tsx @@ -17,7 +17,7 @@ import { EventList, EventListModule } from '../EventList'; // Poll // SpinWheel import { SponsorBanner, SponsorBannerModule } from '../SponsorBanner'; -// StreamBoss +import { StreamBoss, StreamBossModule } from '../StreamBoss'; import { Jar, JarModule } from '../Jar'; import { GameWidget, GameWidgetModule } from '../GameWidget'; import { ViewerCount, ViewerCountModule } from '../ViewerCount'; @@ -48,7 +48,7 @@ export const components = { // Poll // SpinWheel SponsorBanner: [SponsorBanner, SponsorBannerModule], - // StreamBoss + StreamBoss: [StreamBoss, StreamBossModule], TipJar: [Jar, JarModule], ViewerCount: [ViewerCount, ViewerCountModule], GameWidget: [GameWidget, GameWidgetModule], diff --git a/app/components/widgets/StreamBoss.vue b/app/components/widgets/StreamBoss.vue deleted file mode 100644 index d9002d25f708..000000000000 --- a/app/components/widgets/StreamBoss.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - - - diff --git a/app/components/widgets/StreamBoss.vue.ts b/app/components/widgets/StreamBoss.vue.ts deleted file mode 100644 index c70bd1f610c5..000000000000 --- a/app/components/widgets/StreamBoss.vue.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Component } from 'vue-property-decorator'; -import WidgetEditor from 'components/windows/WidgetEditor.vue'; -import WidgetSettings from 'components/widgets/WidgetSettings.vue'; - -import { inputComponents } from 'components/widgets/inputs'; -import VFormGroup from 'components/shared/inputs/VFormGroup.vue'; -import { $t } from 'services/i18n/index'; -import ValidatedForm from 'components/shared/inputs/ValidatedForm'; -import { - IStreamBossCreateOptions, - IStreamBossData, - StreamBossService, -} from 'services/widgets/settings/stream-boss'; - -@Component({ - components: { - WidgetEditor, - VFormGroup, - ValidatedForm, - ...inputComponents, - }, -}) -export default class StreamBoss extends WidgetSettings { - $refs: { - form: ValidatedForm; - }; - - bossCreateOptions: IStreamBossCreateOptions = { - mode: 'fixed', - total_health: 4800, - }; - - textColorTooltip = $t('A hex code for the base text color.'); - - get hasGoal() { - return this.loaded && this.wData.goal; - } - - get multipliersForPlatform() { - const baseEvents = [ - { key: 'donation_multiplier', title: $t('Damage Per Dollar Donation'), isInteger: true }, - ]; - return this.service.multipliersByPlatform().concat(baseEvents); - } - - async saveGoal() { - if (await this.$refs.form.validateAndGetErrorsCount()) return; - await this.service.saveGoal(this.bossCreateOptions); - } - - get navItems() { - return [ - { value: 'goal', label: $t('Goal') }, - { value: 'manage-battle', label: $t('Manage Battle') }, - { value: 'visual', label: $t('Visual Settings') }, - { value: 'source', label: $t('Source') }, - ]; - } - - async resetGoal() { - await this.service.resetGoal(); - } -} diff --git a/app/i18n/en-US/widget-stream-boss.json b/app/i18n/en-US/widget-stream-boss.json index 2b8116e87ab5..398ab122903e 100644 --- a/app/i18n/en-US/widget-stream-boss.json +++ b/app/i18n/en-US/widget-stream-boss.json @@ -7,6 +7,7 @@ "Set Stream Boss Health": "Set Stream Boss Health", "Goal": "Goal", "Manage Battle": "Manage Battle", + "Boss Settings": "Boss Settings", "Starting Health": "Starting Health", "Fixed": "Fixed", "The boss will spawn with the set amount of health everytime.": "The boss will spawn with the set amount of health everytime.", diff --git a/app/services/sources/sources.ts b/app/services/sources/sources.ts index f5e05359cec2..efdd15748b39 100644 --- a/app/services/sources/sources.ts +++ b/app/services/sources/sources.ts @@ -828,7 +828,7 @@ export class SourcesService extends StatefulService { // 'Poll', // 'SpinWheel', 'SponsorBanner', - // 'StreamBoss', + 'StreamBoss', 'TipJar', 'ViewerCount', 'GameWidget', diff --git a/app/services/widgets/settings/stream-boss.ts b/app/services/widgets/settings/stream-boss.ts deleted file mode 100644 index 89eecc2ff661..000000000000 --- a/app/services/widgets/settings/stream-boss.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { WIDGET_INITIAL_STATE } from './widget-settings'; -import { IWidgetData, IWidgetSettings, WidgetDefinitions, WidgetType } from 'services/widgets'; -import { $t } from 'services/i18n'; -import { metadata } from 'components/widgets/inputs/index'; -import { InheritMutations } from 'services/core/stateful-service'; -import { BaseGoalService } from './base-goal'; -import { formMetadata } from 'components/shared/inputs'; -import { TPlatform } from '../../platforms'; - -export interface IStreamBossSettings extends IWidgetSettings { - background_color: string; - bar_bg_color: string; - bar_color: string; - bar_text_color: string; - bg_transparent: boolean; - bit_multiplier: number; - boss_heal: boolean; - donation_multiplier: boolean; - fade_time: number; - follow_multiplier: boolean; - font: string; - incr_amount: string; - kill_animation: string; - overkill_min: number; - overkill_multiplier: number; - skin: string; - sub_multiplier: number; - superchat_multiplier: number; - text_color: string; -} - -export interface IStreamBossData extends IWidgetData { - goal: { - boss_img: string; - boss_name: string; - current_health: number; - mode: string; - multiplier: 1; - percent: number; - total_health: number; - }; - settings: IStreamBossSettings; -} - -type TStreamBossMode = 'fixed' | 'incremental' | 'overkill'; - -export interface IStreamBossCreateOptions { - mode: TStreamBossMode; - total_health: number; -} - -@InheritMutations() -export class StreamBossService extends BaseGoalService { - static initialState = WIDGET_INITIAL_STATE; - - getApiSettings() { - const host = this.getHost(); - return { - type: WidgetType.StreamBoss, - url: WidgetDefinitions[WidgetType.StreamBoss].url(host, this.getWidgetToken()), - previewUrl: `https://${host}/widgets/streamboss?token=${this.getWidgetToken()}`, - webSettingsUrl: `https://${host}/dashboard#/widgets/streamboss`, - settingsUpdateEvent: 'streambossSettingsUpdate', - goalCreateEvent: 'newStreamboss', - goalResetEvent: 'streambossEnd', - dataFetchUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, - settingsSaveUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, - goalUrl: `https://${host}/api/v5/slobs/widget/streamboss`, - testers: ['Follow', 'Subscription', 'Donation', 'Bits'], - customCodeAllowed: true, - customFieldsAllowed: true, - }; - } - - getMetadata() { - return formMetadata({ - // CREATE BOSS - - total_health: metadata.number({ - title: $t('Starting Health'), - required: true, - min: 0, - }), - - mode: metadata.list({ - title: $t('Mode'), - options: [ - { - title: $t('Fixed'), - value: 'fixed', - description: $t('The boss will spawn with the set amount of health everytime.'), - }, - { - title: $t('Incremental'), - value: 'incremental', - description: $t( - 'The boss will have additional health each time he is defeated. The amount is set below.', - ), - }, - { - title: $t('Overkill'), - value: 'overkill', - description: $t( - "The boss' health will change depending on how much damage is dealt on the killing blow. Excess damage multiplied by the multiplier will be the boss' new health. I.e. 150 damage with 100 health remaining and a set multiplier of 3 would result in the new boss having 150 health on spawn. \n Set your multiplier below.", - ), - }, - ], - }), - incr_amount: metadata.number({ title: $t('Increment Amount'), isInteger: true }), - overkill_multiplier: metadata.number({ title: $t('Overkill Multiplier'), isInteger: true }), - overkill_min: metadata.number({ title: $t('Overkill Min Health'), isInteger: true }), - - // SETTINGS - - fade_time: metadata.slider({ - title: $t('Fade Time (s)'), - min: 0, - max: 20, - description: $t('Set to 0 to always appear on screen'), - }), - - boss_heal: metadata.bool({ - title: $t('Damage From Boss Heals'), - }), - - skin: metadata.list({ - title: $t('Theme'), - options: [ - { value: 'default', title: 'Default' }, - { value: 'future', title: 'Future' }, - { value: 'noimg', title: 'No Image' }, - { value: 'pill', title: 'Slim' }, - { value: 'future-curve', title: 'Curved' }, - ], - }), - - kill_animation: metadata.animation({ - title: $t('Kill Animation'), - }), - - bg_transparent: metadata.bool({ - title: $t('Transparent Background'), - }), - - background_color: metadata.color({ - title: $t('Background Color'), - }), - - text_color: metadata.color({ - title: $t('Text Color'), - }), - - bar_text_color: metadata.color({ - title: $t('Health Text Color'), - }), - - bar_color: metadata.color({ - title: $t('Health Bar Color'), - }), - - bar_bg_color: metadata.color({ - title: $t('Health Bar Background Color'), - }), - - font: metadata.fontFamily({ - title: $t('Font'), - }), - }); - } - - multipliersByPlatform(): { key: string; title: string; isInteger: boolean }[] { - const platform = this.userService.platform.type as Exclude< - TPlatform, - 'tiktok' | 'twitter' | 'instagram' | 'kick' | 'patreon' - >; - return { - twitch: [ - { key: 'bit_multiplier', title: $t('Damage Per Bit'), isInteger: true }, - { key: 'sub_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - { key: 'follow_multiplier', title: $t('Damage Per Follower'), isInteger: true }, - ], - facebook: [ - { key: 'follow_multiplier', title: $t('Damage Per Follower'), isInteger: true }, - { key: 'sub_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - ], - youtube: [ - { key: 'sub_multiplier', title: $t('Damage Per Membership'), isInteger: true }, - { key: 'superchat_multiplier', title: $t('Damage Per Superchat Dollar'), isInteger: true }, - { key: 'follow_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - ], - trovo: [ - { key: 'sub_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - { key: 'follow_multiplier', title: $t('Damage Per Follower'), isInteger: true }, - ], - }[platform]; - } -} diff --git a/app/services/widgets/widgets-config.ts b/app/services/widgets/widgets-config.ts index 8f1452092853..e73a48835a8a 100644 --- a/app/services/widgets/widgets-config.ts +++ b/app/services/widgets/widgets-config.ts @@ -25,6 +25,7 @@ export type TWidgetType = | WidgetType.CharityGoal | WidgetType.EventList | WidgetType.TipJar + | WidgetType.StreamBoss | WidgetType.GamePulseWidget; export interface IWidgetConfig { @@ -601,9 +602,32 @@ export function getWidgetsConfig( customFieldsAllowed: true, }, - // StreamBoss: { - // - // }, + [WidgetType.StreamBoss]: { + type: WidgetType.StreamBoss, + + defaultTransform: { + width: 600, + height: 200, + x: 0, + y: 1, + anchor: AnchorPoint.SouthWest, + }, + + settingsWindowSize: { + width: 850, + height: 800, + }, + + url: `https://${host}/widgets/streamboss?token=${token}`, + previewUrl: `https://${host}/widgets/streamboss?token=${token}`, + webSettingsUrl: `https://${host}/dashboard#/widgets/streamboss`, + dataFetchUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, + settingsSaveUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, + settingsUpdateEvent: 'streambossSettingsUpdate', + goalUrl: `https://${host}/api/v5/slobs/widget/streamboss`, + customCodeAllowed: true, + customFieldsAllowed: true, + }, [WidgetType.TipJar]: { type: WidgetType.TipJar, diff --git a/app/services/windows.ts b/app/services/windows.ts index 37828e1d1c46..7d539b3822d5 100644 --- a/app/services/windows.ts +++ b/app/services/windows.ts @@ -54,7 +54,6 @@ import EventFilterMenu from 'components/windows/EventFilterMenu'; import OverlayPlaceholder from 'components/windows/OverlayPlaceholder'; import BrowserSourceInteraction from 'components/windows/BrowserSourceInteraction'; -import StreamBoss from 'components/widgets/StreamBoss.vue'; import MediaShare from 'components/widgets/MediaShare'; import AlertBox from 'components/widgets/AlertBox.vue'; import SpinWheel from 'components/widgets/SpinWheel.vue'; @@ -103,7 +102,6 @@ export function getComponents() { GameOverlayEventFeed, AdvancedStatistics, MultistreamChatInfo, - StreamBoss, MediaShare, AlertBox, SpinWheel,