mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-11-12 22:29:53 -06:00
Remove unused
This commit is contained in:
@@ -25,7 +25,6 @@ export const JSON_COLUMNS: ReadonlySet<string> = new Set([
|
||||
"Tournament.seedingSnapshot",
|
||||
"Tournament.settings",
|
||||
"TournamentAuditLog.metadata",
|
||||
"TournamentBroadcast.overrides",
|
||||
"TournamentBroadcast.settings",
|
||||
"TournamentBroadcast.state",
|
||||
"TournamentMatch.opponentOne",
|
||||
|
||||
@@ -30,7 +30,6 @@ import type {
|
||||
} from "~/db/tables-json";
|
||||
import type { ApiTokenType } from "~/features/api/api-types";
|
||||
import type {
|
||||
BroadcastOverrides,
|
||||
BroadcastSettings,
|
||||
BroadcastStoredState,
|
||||
} from "~/features/broadcast/broadcast-types";
|
||||
@@ -1248,7 +1247,6 @@ export interface TournamentBroadcast {
|
||||
/** Theme, not staged */
|
||||
settings: JSONColumnType<BroadcastSettings>;
|
||||
/** Team and player values the producer edited over the tournament's, not staged */
|
||||
overrides: JSONColumnType<BroadcastOverrides>;
|
||||
/** Chat of the tournament's organizers and streamers producing the broadcast, created once the cast account is set */
|
||||
chatRoomId: number | null;
|
||||
createdAt: Generated<number>;
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("BroadcastRepository.updateSettings", () => {
|
||||
});
|
||||
await BroadcastRepository.updateSettings({
|
||||
...key,
|
||||
patch: { headline: "Finals at 18:00", casters: ["Sendou", "Kalle"] },
|
||||
patch: { headline: "Finals at 18:00", casterIds: [1, 2] },
|
||||
});
|
||||
|
||||
const broadcast = await BroadcastRepository.findByChannel(key);
|
||||
@@ -82,7 +82,7 @@ describe("BroadcastRepository.updateSettings", () => {
|
||||
css: "body { color: red; }",
|
||||
stinger: "burst",
|
||||
headline: "Finals at 18:00",
|
||||
casters: ["Sendou", "Kalle"],
|
||||
casterIds: [1, 2],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -130,64 +130,6 @@ describe("BroadcastRepository.replaceState", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("BroadcastRepository.updateTeamOverrides", () => {
|
||||
beforeEach(async () => {
|
||||
await users.create(1);
|
||||
});
|
||||
|
||||
test("keeps the overrides of other teams", async () => {
|
||||
const key = await createBroadcastKey();
|
||||
|
||||
await BroadcastRepository.updateTeamOverrides({
|
||||
...key,
|
||||
teamId: 1,
|
||||
team: { name: "Team One" },
|
||||
players: [{ userId: 10, override: { weapons: [0, 10] } }],
|
||||
});
|
||||
await BroadcastRepository.updateTeamOverrides({
|
||||
...key,
|
||||
teamId: 2,
|
||||
team: { name: "Team Two" },
|
||||
players: [{ userId: 20, override: { name: "Twenty" } }],
|
||||
});
|
||||
|
||||
const broadcast = await BroadcastRepository.findByChannel(key);
|
||||
expect(broadcast?.overrides).toEqual({
|
||||
teams: { 1: { name: "Team One" }, 2: { name: "Team Two" } },
|
||||
players: { 10: { weapons: [0, 10] }, 20: { name: "Twenty" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("removes the entries set to null", async () => {
|
||||
const key = await createBroadcastKey();
|
||||
await BroadcastRepository.updateTeamOverrides({
|
||||
...key,
|
||||
teamId: 1,
|
||||
team: { name: "Team One" },
|
||||
players: [
|
||||
{ userId: 10, override: { name: "Ten" } },
|
||||
{ userId: 11, override: { name: "Eleven" } },
|
||||
],
|
||||
});
|
||||
|
||||
await BroadcastRepository.updateTeamOverrides({
|
||||
...key,
|
||||
teamId: 1,
|
||||
team: null,
|
||||
players: [
|
||||
{ userId: 10, override: null },
|
||||
{ userId: 11, override: { name: "Eleven" } },
|
||||
],
|
||||
});
|
||||
|
||||
const broadcast = await BroadcastRepository.findByChannel(key);
|
||||
expect(broadcast?.overrides).toEqual({
|
||||
teams: {},
|
||||
players: { 11: { name: "Eleven" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("BroadcastRepository.updateLiveResults", () => {
|
||||
beforeEach(async () => {
|
||||
await users.create(1);
|
||||
|
||||
@@ -10,15 +10,12 @@ import {
|
||||
} from "~/utils/kysely.server";
|
||||
import {
|
||||
BROADCAST_WEAPONS_PER_PLAYER,
|
||||
DEFAULT_BROADCAST_OVERRIDES,
|
||||
DEFAULT_BROADCAST_SETTINGS,
|
||||
} from "./broadcast-constants";
|
||||
import type {
|
||||
BroadcastChannel,
|
||||
BroadcastPlayerOverride,
|
||||
BroadcastSettings,
|
||||
BroadcastStoredState,
|
||||
BroadcastTeamOverride,
|
||||
LiveResults,
|
||||
ProducerState,
|
||||
} from "./broadcast-types";
|
||||
@@ -29,14 +26,13 @@ interface BroadcastKey {
|
||||
channel: Pick<BroadcastChannel, "platform" | "account">;
|
||||
}
|
||||
|
||||
/** Producer state, settings, edited values and chat room of the channel's broadcast, `undefined` before anything was written to it. */
|
||||
/** Producer state, settings and chat room of the channel's broadcast, `undefined` before anything was written to it. */
|
||||
export function findByChannel({ tournamentId, channel }: BroadcastKey) {
|
||||
return db
|
||||
.selectFrom("TournamentBroadcast")
|
||||
.select([
|
||||
"TournamentBroadcast.state",
|
||||
"TournamentBroadcast.settings",
|
||||
"TournamentBroadcast.overrides",
|
||||
"TournamentBroadcast.chatRoomId",
|
||||
])
|
||||
.where("TournamentBroadcast.tournamentId", "=", tournamentId)
|
||||
@@ -76,6 +72,17 @@ export async function findAllByChatRoomIds(chatRoomIds: number[]) {
|
||||
.execute();
|
||||
}
|
||||
|
||||
/** Casters by their user ids, in no particular order. */
|
||||
export function findAllCastersByUserIds(userIds: number[]) {
|
||||
if (userIds.length === 0) return Promise.resolve([]);
|
||||
|
||||
return db
|
||||
.selectFrom("User")
|
||||
.select(["User.id as userId", "User.username as name", "User.pronouns"])
|
||||
.where("User.id", "in", userIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// xxx: do we want to show country?
|
||||
/** Members of the teams with their top weapons (favorites first, then the pool's order), owner first then in joining order. */
|
||||
export function findAllPlayersByTournamentTeamIds(tournamentTeamIds: number[]) {
|
||||
@@ -195,55 +202,6 @@ export async function updateSettings({
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the edited values of one team and its players key by key, so edits of other teams made meanwhile stay.
|
||||
* `null` removes the entry, going back to the tournament's values.
|
||||
*/
|
||||
export async function updateTeamOverrides({
|
||||
teamId,
|
||||
team,
|
||||
players,
|
||||
...key
|
||||
}: BroadcastKey & {
|
||||
teamId: number;
|
||||
team: BroadcastTeamOverride | null;
|
||||
players: Array<{ userId: number; override: BroadcastPlayerOverride | null }>;
|
||||
}) {
|
||||
await insertIfMissing(key);
|
||||
|
||||
const entries = [
|
||||
{ path: `$.teams."${teamId}"`, value: team },
|
||||
...players.map((player) => ({
|
||||
path: `$.players."${player.userId}"`,
|
||||
value: player.override,
|
||||
})),
|
||||
];
|
||||
const removed = entries.filter((entry) => entry.value === null);
|
||||
const set = entries.filter((entry) => entry.value !== null);
|
||||
|
||||
let overrides = sql<string>`"TournamentBroadcast"."overrides"`;
|
||||
if (removed.length > 0) {
|
||||
overrides = sql<string>`json_remove(${overrides}, ${sql.join(
|
||||
removed.map((entry) => sql`${entry.path}`),
|
||||
)})`;
|
||||
}
|
||||
if (set.length > 0) {
|
||||
overrides = sql<string>`json_set(${overrides}, ${sql.join(
|
||||
set.map(
|
||||
(entry) => sql`${entry.path}, json(${JSON.stringify(entry.value)})`,
|
||||
),
|
||||
)})`;
|
||||
}
|
||||
|
||||
await db
|
||||
.updateTable("TournamentBroadcast")
|
||||
.set({ overrides })
|
||||
.where("TournamentBroadcast.tournamentId", "=", key.tournamentId)
|
||||
.where("TournamentBroadcast.platform", "=", key.channel.platform)
|
||||
.where("TournamentBroadcast.account", "=", key.channel.account)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the channel's broadcast its chat room, creating the broadcast too when nothing was written to it yet.
|
||||
* Returns the room's id and whether it was created now.
|
||||
@@ -295,7 +253,6 @@ function insertIfMissing(
|
||||
state: JSON.stringify(Producer.initialState()),
|
||||
// xxx: autofill settings (theme) from the same channel's broadcast in the last tournament of this series
|
||||
settings: JSON.stringify(DEFAULT_BROADCAST_SETTINGS),
|
||||
overrides: JSON.stringify(DEFAULT_BROADCAST_OVERRIDES),
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc.columns(["tournamentId", "platform", "account"]).doNothing(),
|
||||
|
||||
@@ -10,10 +10,8 @@ import {
|
||||
} from "../broadcast-schemas";
|
||||
import { requireBroadcastProducer } from "../broadcast-utils.server";
|
||||
import * as BroadcastHub from "../core/BroadcastHub.server";
|
||||
import * as BroadcastSnapshot from "../core/BroadcastSnapshot.server";
|
||||
import * as CastChannel from "../core/CastChannel";
|
||||
import * as Producer from "../core/Producer";
|
||||
import * as TeamOverrides from "../core/TeamOverrides";
|
||||
|
||||
const STALE_TAKE_MESSAGE =
|
||||
"Someone else took in the meantime, check what is live now";
|
||||
@@ -87,46 +85,6 @@ export const action = defineAction(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "UPDATE_TEAM": {
|
||||
const defaults = notFoundIfNullish(
|
||||
await BroadcastSnapshot.findTeamValues({
|
||||
tournament,
|
||||
teamId: body.teamId,
|
||||
}),
|
||||
);
|
||||
|
||||
await BroadcastRepository.updateTeamOverrides({
|
||||
...key,
|
||||
teamId: body.teamId,
|
||||
...TeamOverrides.fromSubmitted({
|
||||
defaults,
|
||||
submitted: {
|
||||
name: body.name,
|
||||
players: body.players.map((player) => ({
|
||||
userId: player.userId,
|
||||
name: player.name,
|
||||
weapons: player.weapons.map((weapon) => weapon.id),
|
||||
})),
|
||||
},
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "RESET_TEAM": {
|
||||
const defaults = notFoundIfNullish(
|
||||
await BroadcastSnapshot.findTeamValues({
|
||||
tournament,
|
||||
teamId: body.teamId,
|
||||
}),
|
||||
);
|
||||
|
||||
await BroadcastRepository.updateTeamOverrides({
|
||||
...key,
|
||||
teamId: body.teamId,
|
||||
...TeamOverrides.fromSubmitted({ defaults, submitted: defaults }),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "UPDATE_THEME": {
|
||||
await BroadcastRepository.updateSettings({
|
||||
...key,
|
||||
@@ -137,7 +95,7 @@ export const action = defineAction(
|
||||
case "UPDATE_INFO": {
|
||||
await BroadcastRepository.updateSettings({
|
||||
...key,
|
||||
patch: { headline: body.headline ?? "", casters: body.casters },
|
||||
patch: { headline: body.headline ?? "", casterIds: body.casterIds },
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,14 @@ export const BROADCAST_SCENE_PARTS = {
|
||||
"stage",
|
||||
"winner",
|
||||
],
|
||||
casters: ["header", "tournament-name", "title", "camera", "caster-name"],
|
||||
casters: [
|
||||
"header",
|
||||
"tournament-name",
|
||||
"title",
|
||||
"camera",
|
||||
"caster-name",
|
||||
"caster-pronouns",
|
||||
],
|
||||
replays: ["corner", "replay-bug", "replay-dot", "replay-label", "game"],
|
||||
hold: [
|
||||
"tournament-logo",
|
||||
@@ -218,7 +225,7 @@ export const DEFAULT_BROADCAST_SETTINGS = {
|
||||
css: "",
|
||||
stinger: "versus",
|
||||
headline: "",
|
||||
casters: [],
|
||||
casterIds: [],
|
||||
} as const satisfies BroadcastSettings;
|
||||
|
||||
export const BROADCAST_HEADLINE_MAX_LENGTH = 100;
|
||||
@@ -226,17 +233,8 @@ export const BROADCAST_HEADLINE_MAX_LENGTH = 100;
|
||||
// xxx: or 2?
|
||||
export const BROADCAST_MAX_CASTERS = 3;
|
||||
|
||||
export const BROADCAST_CASTER_NAME_MAX_LENGTH = 40;
|
||||
|
||||
export const DEFAULT_BROADCAST_OVERRIDES = { teams: {}, players: {} } as const;
|
||||
|
||||
export const BROADCAST_WEAPONS_PER_PLAYER = 3;
|
||||
|
||||
export const BROADCAST_PLAYER_NAME_MAX_LENGTH = 32;
|
||||
|
||||
/** Upper bound of the edit form's roster, rosters are capped well below by the tournament. */
|
||||
export const BROADCAST_MAX_PLAYERS_PER_TEAM = 20;
|
||||
|
||||
/** Days the producers' chat lives from the tournament's start, or from when the channel was set if that's later. */
|
||||
export const BROADCAST_CHAT_ROOM_LIFESPAN_DAYS = 7;
|
||||
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import * as v from "valibot";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import {
|
||||
array,
|
||||
fieldset,
|
||||
idConstant,
|
||||
select,
|
||||
stringConstant,
|
||||
textAreaOptional,
|
||||
textField,
|
||||
textFieldOptional,
|
||||
weaponPool,
|
||||
userSearch,
|
||||
} from "~/form/fields";
|
||||
import { _action, coerceNumber, id } from "~/utils/schema";
|
||||
import {
|
||||
BROADCAST_CASTER_NAME_MAX_LENGTH,
|
||||
BROADCAST_CSS_MAX_LENGTH,
|
||||
BROADCAST_HEADLINE_MAX_LENGTH,
|
||||
BROADCAST_INK_COLORS,
|
||||
BROADCAST_MAX_CASTERS,
|
||||
BROADCAST_MAX_PLAYERS_PER_TEAM,
|
||||
BROADCAST_PLAYER_NAME_MAX_LENGTH,
|
||||
BROADCAST_SCENES,
|
||||
BROADCAST_STINGERS,
|
||||
BROADCAST_TRANSITIONS,
|
||||
BROADCAST_WEAPONS_PER_PLAYER,
|
||||
} from "./broadcast-constants";
|
||||
|
||||
const nonNegativeInteger = v.pipe(coerceNumber(), v.integer(), v.minValue(0));
|
||||
@@ -61,39 +53,11 @@ export const broadcastInfoFormSchema = v.object({
|
||||
bottomText: "bottomTexts.broadcastHeadline",
|
||||
maxLength: BROADCAST_HEADLINE_MAX_LENGTH,
|
||||
}),
|
||||
casters: array({
|
||||
casterIds: array({
|
||||
label: "labels.broadcastCasters",
|
||||
bottomText: "bottomTexts.broadcastCasters",
|
||||
max: BROADCAST_MAX_CASTERS,
|
||||
field: textField({ maxLength: BROADCAST_CASTER_NAME_MAX_LENGTH }),
|
||||
}),
|
||||
});
|
||||
|
||||
export const broadcastTeamFormSchema = v.object({
|
||||
_action: stringConstant("UPDATE_TEAM"),
|
||||
teamId: idConstant(),
|
||||
name: textField({
|
||||
label: "labels.teamName",
|
||||
maxLength: TOURNAMENT.TEAM_NAME_MAX_LENGTH,
|
||||
}),
|
||||
players: array({
|
||||
label: "labels.broadcastPlayers",
|
||||
max: BROADCAST_MAX_PLAYERS_PER_TEAM,
|
||||
addable: false,
|
||||
field: fieldset({
|
||||
fields: v.object({
|
||||
userId: idConstant(),
|
||||
name: textField({
|
||||
label: "labels.name",
|
||||
maxLength: BROADCAST_PLAYER_NAME_MAX_LENGTH,
|
||||
}),
|
||||
weapons: weaponPool({
|
||||
label: "labels.broadcastWeapons",
|
||||
maxCount: BROADCAST_WEAPONS_PER_PLAYER,
|
||||
disableFavorites: true,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
field: userSearch({ label: "labels.user" }),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -122,8 +86,6 @@ export const broadcastActionSchema = v.union([
|
||||
matchId: id,
|
||||
resultCount: nonNegativeInteger,
|
||||
}),
|
||||
v.object({ _action: _action("RESET_TEAM"), teamId: id }),
|
||||
broadcastThemeFormSchema,
|
||||
broadcastInfoFormSchema,
|
||||
broadcastTeamFormSchema,
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Pronouns } from "~/db/tables-json";
|
||||
import type { StreamPlatform } from "~/features/live-streams/live-streams-constants";
|
||||
import type {
|
||||
MainWeaponId,
|
||||
@@ -67,25 +68,8 @@ export interface BroadcastSettings {
|
||||
stinger: BroadcastStinger;
|
||||
/** Message of the full screen scenes, e.g. "Top 8 starts at 18:00 CEST". */
|
||||
headline: string;
|
||||
/** Names under the camera frames of the casters scene, one frame per caster. */
|
||||
casters: string[];
|
||||
}
|
||||
|
||||
/** Team and player values the producer edited, shown instead of the tournament's in both live and next. */
|
||||
export interface BroadcastOverrides {
|
||||
/** By tournament team id. */
|
||||
teams: Record<number, BroadcastTeamOverride>;
|
||||
/** By user id. */
|
||||
players: Record<number, BroadcastPlayerOverride>;
|
||||
}
|
||||
|
||||
export interface BroadcastTeamOverride {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface BroadcastPlayerOverride {
|
||||
name?: string;
|
||||
weapons?: MainWeaponId[];
|
||||
/** Users under the camera frames of the casters scene, one frame per caster. */
|
||||
casterIds: number[];
|
||||
}
|
||||
|
||||
/** The cast account a broadcast is for. */
|
||||
@@ -101,6 +85,8 @@ export interface BroadcastChannel {
|
||||
export interface BroadcastState extends BroadcastStoredState {
|
||||
v: 1;
|
||||
settings: BroadcastSettings;
|
||||
/** Users of `settings.casterIds` in order, ones that no longer exist left out. */
|
||||
casters: BroadcastCaster[];
|
||||
tournament: {
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
@@ -163,8 +149,12 @@ export interface BroadcastTeam {
|
||||
logoUrl: string | null;
|
||||
seed: number | null;
|
||||
players: BroadcastPlayer[];
|
||||
/** The producer edited the team's or its players' values. */
|
||||
isEdited: boolean;
|
||||
}
|
||||
|
||||
export interface BroadcastCaster {
|
||||
userId: number;
|
||||
name: string;
|
||||
pronouns: Pronouns | null;
|
||||
}
|
||||
|
||||
export interface BroadcastPlayer {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Pronouns } from "~/db/tables-json";
|
||||
import {
|
||||
BROADCAST_INK_COLORS,
|
||||
type BROADCAST_SCENE_PARTS,
|
||||
@@ -74,6 +75,11 @@ export function inkColorVars(
|
||||
};
|
||||
}
|
||||
|
||||
/** Pronouns as shown next to a name, e.g. "he/him". */
|
||||
export function pronounsLabel(pronouns: Pronouns) {
|
||||
return `${pronouns.subject}/${pronouns.object}`;
|
||||
}
|
||||
|
||||
/** Bracket and round, e.g. "Main bracket · Winners round 2". */
|
||||
export function roundLabel(
|
||||
match: Pick<BroadcastMatch, "bracketName" | "roundName">,
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
--bc-surface-high: var(--color-bg-higher);
|
||||
--bc-border: var(--color-border);
|
||||
--bc-text: var(--color-text);
|
||||
--bc-text-muted: color-mix(in oklch, var(--color-text) 45%, var(--color-text-high));
|
||||
--bc-text-muted: color-mix(
|
||||
in oklch,
|
||||
var(--color-text) 45%,
|
||||
var(--color-text-high)
|
||||
);
|
||||
--bc-accent: var(--color-fill-accent);
|
||||
--bc-accent-text: var(--color-fg-on-accent);
|
||||
--bc-team-a: var(--color-chart-alpha);
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pronouns {
|
||||
margin-inline-start: 14px;
|
||||
color: var(--bc-text-muted);
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.matchup {
|
||||
--bc-enter-y: var(--bc-enter-distance);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BROADCAST_CANVAS } from "../broadcast-constants";
|
||||
import type { BroadcastState, BroadcastView } from "../broadcast-types";
|
||||
import { scenePart } from "../broadcast-utils";
|
||||
import { pronounsLabel, scenePart } from "../broadcast-utils";
|
||||
import styles from "./CastersScene.module.css";
|
||||
import { Matchup } from "./Matchup";
|
||||
import { SceneBackdrop } from "./SceneBackdrop";
|
||||
@@ -41,7 +41,7 @@ export function CastersScene({
|
||||
view: BroadcastView;
|
||||
}) {
|
||||
const { t } = useTranslation(["broadcast"]);
|
||||
const casters = state.settings.casters;
|
||||
const casters = state.casters;
|
||||
const frames =
|
||||
FRAME_LAYOUTS[casters.length] ?? FRAME_LAYOUTS[DEFAULT_FRAME_COUNT];
|
||||
|
||||
@@ -75,7 +75,12 @@ export function CastersScene({
|
||||
>
|
||||
{casters[i] ? (
|
||||
<div className={styles.name} {...part("caster-name")}>
|
||||
{casters[i]}
|
||||
{casters[i].name}
|
||||
{casters[i].pronouns ? (
|
||||
<span className={styles.pronouns} {...part("caster-pronouns")}>
|
||||
{pronounsLabel(casters[i].pronouns)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,12 @@ import type {
|
||||
BroadcastState,
|
||||
BroadcastView,
|
||||
} from "../broadcast-types";
|
||||
import { type MatchSide, matchSides, scenePart } from "../broadcast-utils";
|
||||
import {
|
||||
type MatchSide,
|
||||
matchSides,
|
||||
pronounsLabel,
|
||||
scenePart,
|
||||
} from "../broadcast-utils";
|
||||
import styles from "./GameScene.module.css";
|
||||
import { SetLength } from "./SetLength";
|
||||
|
||||
@@ -89,7 +94,13 @@ function TeamRow({
|
||||
|
||||
function InfoBar({ state }: { state: BroadcastState }) {
|
||||
const match = state.match;
|
||||
const casters = state.settings.casters.join(" // ");
|
||||
const casters = state.casters
|
||||
.map((caster) =>
|
||||
caster.pronouns
|
||||
? `${caster.name} (${pronounsLabel(caster.pronouns)})`
|
||||
: caster.name,
|
||||
)
|
||||
.join(" // ");
|
||||
|
||||
return (
|
||||
<div className={styles.infoBar} {...part("info-bar")}>
|
||||
|
||||
@@ -6,14 +6,11 @@ import * as TournamentMatchRepository from "~/features/tournament-match/Tourname
|
||||
import { inGameNameWithoutDiscriminator } from "~/utils/strings";
|
||||
import { avatarUrl } from "~/utils/urls";
|
||||
import * as BroadcastRepository from "../BroadcastRepository.server";
|
||||
import {
|
||||
DEFAULT_BROADCAST_OVERRIDES,
|
||||
DEFAULT_BROADCAST_SETTINGS,
|
||||
} from "../broadcast-constants";
|
||||
import { DEFAULT_BROADCAST_SETTINGS } from "../broadcast-constants";
|
||||
import type {
|
||||
BroadcastCaster,
|
||||
BroadcastChannel,
|
||||
BroadcastMatch,
|
||||
BroadcastOverrides,
|
||||
BroadcastPlayer,
|
||||
BroadcastState,
|
||||
BroadcastTeam,
|
||||
@@ -22,7 +19,6 @@ import type {
|
||||
import * as CastChannel from "./CastChannel";
|
||||
import * as Producer from "./Producer";
|
||||
import * as SetProgress from "./SetProgress";
|
||||
import * as TeamOverrides from "./TeamOverrides";
|
||||
|
||||
type PlayerRow = Awaited<
|
||||
ReturnType<typeof BroadcastRepository.findAllPlayersByTournamentTeamIds>
|
||||
@@ -50,6 +46,8 @@ export async function build({
|
||||
tournamentId,
|
||||
channel,
|
||||
});
|
||||
// settings added later are missing from broadcasts written before
|
||||
const settings = { ...DEFAULT_BROADCAST_SETTINGS, ...stored?.settings };
|
||||
const featuredMatchId = CastChannel.featuredMatchId({
|
||||
castedMatchesInfo: tournament.ctx.castedMatchesInfo,
|
||||
channel,
|
||||
@@ -60,8 +58,8 @@ export async function build({
|
||||
snapshot: {
|
||||
v: 1,
|
||||
...(stored?.state ?? Producer.initialState()),
|
||||
// settings added later are missing from broadcasts written before
|
||||
settings: { ...DEFAULT_BROADCAST_SETTINGS, ...stored?.settings },
|
||||
settings,
|
||||
casters: await casters(settings.casterIds),
|
||||
tournament: {
|
||||
name: tournament.ctx.name,
|
||||
logoUrl: tournament.ctx.logoUrl,
|
||||
@@ -70,7 +68,6 @@ export async function build({
|
||||
? await featuredMatch({
|
||||
tournament,
|
||||
matchId: featuredMatchId,
|
||||
overrides: stored?.overrides ?? DEFAULT_BROADCAST_OVERRIDES,
|
||||
liveResults: stored?.state.liveResults ?? null,
|
||||
})
|
||||
: null,
|
||||
@@ -78,41 +75,22 @@ export async function build({
|
||||
};
|
||||
}
|
||||
|
||||
/** The team's editable values as the tournament has them, without the producer's edits. `null` for a team not in the tournament. */
|
||||
export async function findTeamValues({
|
||||
tournament,
|
||||
teamId,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
teamId: number;
|
||||
}): Promise<TeamOverrides.TeamValues | null> {
|
||||
const tournamentTeam = tournament.teamById(teamId);
|
||||
if (!tournamentTeam) return null;
|
||||
async function casters(userIds: number[]): Promise<BroadcastCaster[]> {
|
||||
const users = await BroadcastRepository.findAllCastersByUserIds(userIds);
|
||||
|
||||
const players = await BroadcastRepository.findAllPlayersByTournamentTeamIds([
|
||||
teamId,
|
||||
]);
|
||||
|
||||
return {
|
||||
name: tournamentTeam.name,
|
||||
players: rosterPlayers({ players, tournament, tournamentTeam }).map(
|
||||
(player) => {
|
||||
const { userId, name, weapons } = toBroadcastPlayer(player);
|
||||
return { userId, name, weapons };
|
||||
},
|
||||
),
|
||||
};
|
||||
return userIds.flatMap((userId) => {
|
||||
const user = users.find((candidate) => candidate.userId === userId);
|
||||
return user ? [user] : [];
|
||||
});
|
||||
}
|
||||
|
||||
async function featuredMatch({
|
||||
tournament,
|
||||
matchId,
|
||||
overrides,
|
||||
liveResults,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
matchId: number;
|
||||
overrides: BroadcastOverrides;
|
||||
liveResults: LiveResults | null;
|
||||
}): Promise<BroadcastMatch | null> {
|
||||
const match = await TournamentMatchRepository.findMatchById(matchId);
|
||||
@@ -153,23 +131,19 @@ async function featuredMatch({
|
||||
: null;
|
||||
if (!opponent?.id || !tournamentTeam) return null;
|
||||
|
||||
return TeamOverrides.apply(
|
||||
{
|
||||
id: tournamentTeam.id,
|
||||
name: tournamentTeam.name,
|
||||
logoUrl: tournamentTeam.logoUrl,
|
||||
seed: tournamentTeam.seed,
|
||||
players: rosterPlayers({
|
||||
players: players.filter(
|
||||
(player) => player.tournamentTeamId === tournamentTeam.id,
|
||||
),
|
||||
tournament,
|
||||
tournamentTeam,
|
||||
}).map(toBroadcastPlayer),
|
||||
isEdited: false,
|
||||
},
|
||||
overrides,
|
||||
);
|
||||
return {
|
||||
id: tournamentTeam.id,
|
||||
name: tournamentTeam.name,
|
||||
logoUrl: tournamentTeam.logoUrl,
|
||||
seed: tournamentTeam.seed,
|
||||
players: rosterPlayers({
|
||||
players: players.filter(
|
||||
(player) => player.tournamentTeamId === tournamentTeam.id,
|
||||
),
|
||||
tournament,
|
||||
tournamentTeam,
|
||||
}).map(toBroadcastPlayer),
|
||||
};
|
||||
};
|
||||
|
||||
const names = tournament.matchContextNamesById(matchId);
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { BroadcastTeam } from "../broadcast-types";
|
||||
import * as TeamOverrides from "./TeamOverrides";
|
||||
|
||||
const team: BroadcastTeam = {
|
||||
id: 1,
|
||||
name: "Team One",
|
||||
logoUrl: null,
|
||||
seed: 1,
|
||||
isEdited: false,
|
||||
players: [
|
||||
{
|
||||
userId: 10,
|
||||
name: "Ten",
|
||||
avatarUrl: null,
|
||||
weapons: [0, 10],
|
||||
},
|
||||
{
|
||||
userId: 11,
|
||||
name: "Eleven",
|
||||
avatarUrl: null,
|
||||
weapons: [20],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const defaults: TeamOverrides.TeamValues = {
|
||||
name: team.name,
|
||||
players: team.players.map(({ userId, name, weapons }) => ({
|
||||
userId,
|
||||
name,
|
||||
weapons,
|
||||
})),
|
||||
};
|
||||
|
||||
describe("TeamOverrides.apply", () => {
|
||||
test("shows the edited values over the tournament's", () => {
|
||||
const applied = TeamOverrides.apply(team, {
|
||||
teams: { 1: { name: "Renamed" } },
|
||||
players: { 11: { weapons: [40] } },
|
||||
});
|
||||
|
||||
expect(applied.name).toBe("Renamed");
|
||||
expect(applied.players[0]).toEqual(team.players[0]);
|
||||
expect(applied.players[1]).toEqual({ ...team.players[1], weapons: [40] });
|
||||
expect(applied.isEdited).toBe(true);
|
||||
});
|
||||
|
||||
test("ignores overrides of other teams and their players", () => {
|
||||
const applied = TeamOverrides.apply(team, {
|
||||
teams: { 2: { name: "Other" } },
|
||||
players: { 20: { name: "Other player" } },
|
||||
});
|
||||
|
||||
expect(applied).toEqual(team);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TeamOverrides.fromSubmitted", () => {
|
||||
test("stores nothing when the submitted values are the tournament's", () => {
|
||||
expect(
|
||||
TeamOverrides.fromSubmitted({ defaults, submitted: defaults }),
|
||||
).toEqual({
|
||||
team: null,
|
||||
players: [
|
||||
{ userId: 10, override: null },
|
||||
{ userId: 11, override: null },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("stores only the fields that differ", () => {
|
||||
const overrides = TeamOverrides.fromSubmitted({
|
||||
defaults,
|
||||
submitted: {
|
||||
name: "Renamed",
|
||||
players: [
|
||||
{ userId: 10, name: "Ten", weapons: [10, 0] },
|
||||
{ userId: 11, name: "Eleventh", weapons: [20] },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(overrides).toEqual({
|
||||
team: { name: "Renamed" },
|
||||
players: [
|
||||
{ userId: 10, override: { weapons: [10, 0] } },
|
||||
{ userId: 11, override: { name: "Eleventh" } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("ignores submitted players not on the team", () => {
|
||||
const overrides = TeamOverrides.fromSubmitted({
|
||||
defaults,
|
||||
submitted: {
|
||||
...defaults,
|
||||
players: [
|
||||
...defaults.players,
|
||||
{ userId: 99, name: "Intruder", weapons: [] },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(overrides.players.map((player) => player.userId)).toEqual([10, 11]);
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import * as R from "remeda";
|
||||
import type {
|
||||
BroadcastOverrides,
|
||||
BroadcastPlayer,
|
||||
BroadcastPlayerOverride,
|
||||
BroadcastTeam,
|
||||
BroadcastTeamOverride,
|
||||
} from "../broadcast-types";
|
||||
|
||||
/** The editable values of a team, as the tournament has them or as the producer submitted them. */
|
||||
export interface TeamValues {
|
||||
name: string;
|
||||
players: Array<Pick<BroadcastPlayer, "userId" | "name" | "weapons">>;
|
||||
}
|
||||
|
||||
/** The team with the producer's edits applied over the tournament's values. */
|
||||
export function apply(
|
||||
team: BroadcastTeam,
|
||||
overrides: BroadcastOverrides,
|
||||
): BroadcastTeam {
|
||||
const teamOverride = overrides.teams[team.id];
|
||||
|
||||
return {
|
||||
...team,
|
||||
...teamOverride,
|
||||
players: team.players.map((player) => ({
|
||||
...player,
|
||||
...overrides.players[player.userId],
|
||||
})),
|
||||
isEdited:
|
||||
Boolean(teamOverride) ||
|
||||
team.players.some((player) => overrides.players[player.userId]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides to store for the submitted values: only what differs from the tournament's values, `null` where
|
||||
* nothing does (so later changes to the tournament's values show again). Submitted players not on the team are ignored.
|
||||
*/
|
||||
export function fromSubmitted({
|
||||
defaults,
|
||||
submitted,
|
||||
}: {
|
||||
defaults: TeamValues;
|
||||
submitted: TeamValues;
|
||||
}): {
|
||||
team: BroadcastTeamOverride | null;
|
||||
players: Array<{ userId: number; override: BroadcastPlayerOverride | null }>;
|
||||
} {
|
||||
return {
|
||||
team: submitted.name !== defaults.name ? { name: submitted.name } : null,
|
||||
players: defaults.players.map((player) => {
|
||||
const submittedPlayer = submitted.players.find(
|
||||
(candidate) => candidate.userId === player.userId,
|
||||
);
|
||||
const override: BroadcastPlayerOverride = {};
|
||||
if (submittedPlayer && submittedPlayer.name !== player.name) {
|
||||
override.name = submittedPlayer.name;
|
||||
}
|
||||
if (
|
||||
submittedPlayer &&
|
||||
!R.isDeepEqual(submittedPlayer.weapons, player.weapons)
|
||||
) {
|
||||
override.weapons = submittedPlayer.weapons;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: player.userId,
|
||||
override: R.isEmpty(override) ? null : override,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -118,12 +118,6 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.teams {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr));
|
||||
gap: var(--s-8);
|
||||
}
|
||||
|
||||
.viewHeader {
|
||||
font-size: var(--font-md);
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Map as MapIcon,
|
||||
Mic,
|
||||
Play,
|
||||
RotateCcw,
|
||||
TriangleAlert,
|
||||
Trophy,
|
||||
Users,
|
||||
@@ -43,13 +42,11 @@ import { useBroadcastStream } from "../broadcast-hooks";
|
||||
import {
|
||||
broadcastActionSchema,
|
||||
broadcastInfoFormSchema,
|
||||
broadcastTeamFormSchema,
|
||||
broadcastThemeFormSchema,
|
||||
} from "../broadcast-schemas";
|
||||
import type {
|
||||
BroadcastScene,
|
||||
BroadcastState,
|
||||
BroadcastTeam,
|
||||
BroadcastView,
|
||||
} from "../broadcast-types";
|
||||
import { broadcastOverlayPage, broadcastStreamUrl } from "../broadcast-urls";
|
||||
@@ -175,13 +172,13 @@ export default function BroadcastDashboardPage() {
|
||||
schema={broadcastInfoFormSchema}
|
||||
defaultValues={{
|
||||
headline: state.settings.headline || null,
|
||||
casters: state.settings.casters,
|
||||
casterIds: state.settings.casterIds,
|
||||
}}
|
||||
>
|
||||
{({ FormField }) => (
|
||||
<>
|
||||
<FormField name="headline" />
|
||||
<FormField name="casters" />
|
||||
<FormField name="casterIds" />
|
||||
</>
|
||||
)}
|
||||
</SendouForm>
|
||||
@@ -367,27 +364,18 @@ function Teams({ state }: { state: BroadcastState }) {
|
||||
return (
|
||||
<SendouSection title={t("broadcast:dashboard.teams.header")}>
|
||||
<div className="stack md">
|
||||
<div className="stack horizontal md items-center justify-between flex-wrap">
|
||||
<p className="text-sm text-lighter">
|
||||
{t("broadcast:dashboard.teams.description")}
|
||||
</p>
|
||||
<ActionButton
|
||||
schema={broadcastActionSchema}
|
||||
action="SET_NEXT_SWAP_SIDES"
|
||||
fields={{ swapSides: !state.next.swapSides }}
|
||||
variant={state.next.swapSides ? "primary" : "outlined"}
|
||||
size="small"
|
||||
icon={<ArrowLeftRight />}
|
||||
>
|
||||
{t("broadcast:dashboard.swapSides")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<ActionButton
|
||||
schema={broadcastActionSchema}
|
||||
action="SET_NEXT_SWAP_SIDES"
|
||||
fields={{ swapSides: !state.next.swapSides }}
|
||||
variant={state.next.swapSides ? "primary" : "outlined"}
|
||||
size="small"
|
||||
icon={<ArrowLeftRight />}
|
||||
formClassName="self-start"
|
||||
>
|
||||
{t("broadcast:dashboard.swapSides")}
|
||||
</ActionButton>
|
||||
<InkColors state={state} />
|
||||
<div className={styles.teams}>
|
||||
{state.match.teams.map((team, i) =>
|
||||
team ? <TeamEditor key={team.id} team={team} /> : <div key={i} />,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SendouSection>
|
||||
);
|
||||
@@ -470,50 +458,6 @@ function InkColors({ state }: { state: BroadcastState }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TeamEditor({ team }: { team: BroadcastTeam }) {
|
||||
const { t } = useTranslation(["broadcast"]);
|
||||
const defaultValues = {
|
||||
teamId: team.id,
|
||||
name: team.name,
|
||||
players: team.players.map((player) => ({
|
||||
userId: player.userId,
|
||||
name: player.name,
|
||||
weapons: player.weapons.map((id) => ({ id, isFavorite: false })),
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="stack md">
|
||||
{team.isEdited ? (
|
||||
<ActionButton
|
||||
schema={broadcastActionSchema}
|
||||
action="RESET_TEAM"
|
||||
fields={{ teamId: team.id }}
|
||||
variant="minimal"
|
||||
size="small"
|
||||
icon={<RotateCcw />}
|
||||
formClassName="self-start"
|
||||
>
|
||||
{t("broadcast:dashboard.teams.reset")}
|
||||
</ActionButton>
|
||||
) : null}
|
||||
<SendouForm
|
||||
// remounts with the saved values, e.g. after another producer's edit
|
||||
key={JSON.stringify(defaultValues)}
|
||||
schema={broadcastTeamFormSchema}
|
||||
defaultValues={defaultValues}
|
||||
>
|
||||
{({ FormField }) => (
|
||||
<>
|
||||
<FormField name="name" />
|
||||
<FormField name="players" canRemoveItem={() => false} />
|
||||
</>
|
||||
)}
|
||||
</SendouForm>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultsNotOnAir({ state }: { state: BroadcastState }) {
|
||||
const { t } = useTranslation(["broadcast"]);
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
"dashboard.showResults": "Show now",
|
||||
"dashboard.gameplay": "Gameplay footage",
|
||||
"dashboard.teams.header": "Teams",
|
||||
"dashboard.teams.description": "Edits show on both live and next right away. Values you leave as they are keep following the tournament.",
|
||||
"dashboard.teams.reset": "Reset to tournament values",
|
||||
"dashboard.health.sources_one": "OBS: {{count}} source connected",
|
||||
"dashboard.health.sources_other": "OBS: {{count}} sources connected",
|
||||
"dashboard.health.disconnected": "Disconnected from sendou.ink, reconnecting...",
|
||||
|
||||
@@ -530,8 +530,6 @@
|
||||
"errors.canNotSetIfStartingNow": "Can not be set if starting now",
|
||||
"labels.broadcastCss": "Custom CSS",
|
||||
"bottomTexts.broadcastCss": "Applied after the default styles. Override the variables on [data-broadcast], redefine the bc-* @keyframes or target parts like [data-scene=\"game\"] [data-part=\"team-name\"].",
|
||||
"labels.broadcastPlayers": "Players",
|
||||
"labels.broadcastWeapons": "Weapons",
|
||||
"labels.broadcastStinger": "Stinger",
|
||||
"bottomTexts.broadcastStinger": "Full screen animation with the upcoming match's info, covering the scene change on Takes that use the stinger transition.",
|
||||
"options.broadcastStinger.versus": "Versus: the teams clash from both sides",
|
||||
@@ -541,5 +539,5 @@
|
||||
"labels.broadcastHeadline": "Headline",
|
||||
"bottomTexts.broadcastHeadline": "Shown on the full screen scenes, e.g. Starting soon and Break.",
|
||||
"labels.broadcastCasters": "Casters",
|
||||
"bottomTexts.broadcastCasters": "Shown as written, e.g. \"Sendou - he/him\". The Game scene lists them next to the round, the Casters scene has a camera frame for each (put your camera sources under the browser source in OBS)."
|
||||
"bottomTexts.broadcastCasters": "Shown with the pronouns of their profile. The Game scene lists them next to the round, the Casters scene has a camera frame for each (put your camera sources under the browser source in OBS)."
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
/** Broadcast overlays: one row per cast account of a tournament holding its producer state, theme, edited team values and chat room. */
|
||||
/** Broadcast overlays: one row per cast account of a tournament holding its producer state, theme and chat room. */
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
@@ -13,7 +13,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.addColumn("account", "text", (col) => col.notNull())
|
||||
.addColumn("state", "text", (col) => col.notNull())
|
||||
.addColumn("settings", "text", (col) => col.notNull())
|
||||
.addColumn("overrides", "text", (col) => col.notNull())
|
||||
.addColumn("chatRoomId", "integer", (col) =>
|
||||
col.references("ChatRoom.id").onDelete("set null"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user