For v30: Add new modifiers (Hard nations and 25M Starting Gold) 🙂 (#3316)

## Description:

Adds two new public game modifiers for variety and improves compact map
eligibility for team games.

### New Modifiers

**Hard Nations (`isHardNations`)**
- We need this modifier for HvN, because medium nations are easier now
(will result in a much higher human winrate)
- In a discord discussion we concluded that HvN should generally be
easier (higher winrate than 50%, so players are less frustated)
- Thats why only 20% of HvN games have the hard nations modifier (for
now)
- For PvPvE enjoyers, the modifier is also active in FFA games => (Only
2.5% chance, and 1 ticket in `SPECIAL_MODIFIER_POOL`)

**25M Starting Gold (`startingGoldHigh`)**
- Some people in the main discord wanted this modifier, and it will
result in crazy games
- Rare special-only modifier (1 ticket in pool); mutually exclusive with
5M starting gold via `MUTUALLY_EXCLUSIVE_MODIFIERS`
- Disables nations (they lack PVP immunity, so 25M gold doesn't work
well with them)
- Excluded from HumansVsNations games (since it disables nations)
- Spawn immunity set to **2 minutes 30 seconds** (vs 30s for 5M gold),
so people can spend the gold and prepare

### Other Changes

- **Improved `supportsCompactMapForTeams`**: Replaced the hard `smallest
>= 50` land-tile cutoff with a per-team-config calculation that
simulates worst-case compact player count and checks every team gets at
least 2 players.
- **HvN spawn immunity**: Always 5 seconds in both regular and special
lobbies (to get rid of a confusing PVP immunity HeadsUpMessage in 5M
starting gold games)
- **Regular public lobby random spawn modifier probabilty**: Reduced
from 10% to 5% (Because of the new modifier, so there aren't too many
modifiers in non-special-lobbies, should only occur sometimes there)
- Rebalanced `SPECIAL_MODIFIER_POOL` a bit

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
This commit is contained in:
FloPinguin
2026-03-01 20:12:38 -08:00
committed by GitHub
parent e1125e0c37
commit 417fa0fe09
7 changed files with 207 additions and 57 deletions
+4 -2
View File
@@ -427,7 +427,8 @@
"teams_Quads": "Quads (teams of 4)", "teams_Quads": "Quads (teams of 4)",
"teams_Humans Vs Nations": "Humans vs Nations", "teams_Humans Vs Nations": "Humans vs Nations",
"starting_gold": "Starting gold", "starting_gold": "Starting gold",
"crowded": "Crowded modifier" "crowded": "Crowded modifier",
"hard_nations": "Hard Nations"
}, },
"team_colors": { "team_colors": {
"red": "Red", "red": "Red",
@@ -469,7 +470,8 @@
"random_spawn": "Random Spawn", "random_spawn": "Random Spawn",
"compact_map": "Compact Map", "compact_map": "Compact Map",
"crowded": "Crowded", "crowded": "Crowded",
"starting_gold": "5M Starting Gold" "hard_nations": "Hard Nations",
"starting_gold": "{amount}M Starting Gold"
}, },
"select_lang": { "select_lang": {
"title": "Select Language" "title": "Select Language"
+7 -13
View File
@@ -5,7 +5,6 @@ import {
GameMapType, GameMapType,
GameMode, GameMode,
HumansVsNations, HumansVsNations,
PublicGameModifiers,
Quads, Quads,
Trios, Trios,
} from "../core/game/Game"; } from "../core/game/Game";
@@ -16,7 +15,12 @@ import { PublicLobbySocket } from "./LobbySocket";
import { JoinLobbyEvent } from "./Main"; import { JoinLobbyEvent } from "./Main";
import { SinglePlayerModal } from "./SinglePlayerModal"; import { SinglePlayerModal } from "./SinglePlayerModal";
import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { getMapName, renderDuration, translateText } from "./Utils"; import {
getMapName,
getModifierLabels,
renderDuration,
translateText,
} from "./Utils";
const CARD_BG = "bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)]"; const CARD_BG = "bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)]";
@@ -198,7 +202,7 @@ export class GameModeSelector extends LitElement {
const mapName = getMapName(lobby.gameConfig?.gameMap); const mapName = getMapName(lobby.gameConfig?.gameMap);
const modifierLabels = this.getModifierLabels( const modifierLabels = getModifierLabels(
lobby.gameConfig?.publicGameModifiers, lobby.gameConfig?.publicGameModifiers,
); );
// Sort by length for visual consistency (shorter labels first) // Sort by length for visual consistency (shorter labels first)
@@ -283,16 +287,6 @@ export class GameModeSelector extends LitElement {
); );
} }
private getModifierLabels(mods: PublicGameModifiers | undefined): string[] {
if (!mods) return [];
return [
mods.isRandomSpawn && translateText("public_game_modifier.random_spawn"),
mods.isCompact && translateText("public_game_modifier.compact_map"),
mods.isCrowded && translateText("public_game_modifier.crowded"),
mods.startingGold && translateText("public_game_modifier.starting_gold"),
].filter((x): x is string => !!x);
}
private getLobbyTitle(lobby: PublicGameInfo): string { private getLobbyTitle(lobby: PublicGameInfo): string {
const config = lobby.gameConfig!; const config = lobby.gameConfig!;
if (config.gameMode === GameMode.FFA) { if (config.gameMode === GameMode.FFA) {
+14 -1
View File
@@ -109,6 +109,8 @@ export interface ModifierInfo {
labelKey: string; labelKey: string;
/** Translation key for badge/short label (e.g. "public_game_modifier.random_spawn") */ /** Translation key for badge/short label (e.g. "public_game_modifier.random_spawn") */
badgeKey: string; badgeKey: string;
/** Parameters to pass to translateText for the badge key */
badgeParams?: Record<string, string | number>;
/** The raw value if applicable (e.g. startingGold amount) */ /** The raw value if applicable (e.g. startingGold amount) */
value?: number; value?: number;
} }
@@ -139,10 +141,19 @@ export function getActiveModifiers(
badgeKey: "public_game_modifier.crowded", badgeKey: "public_game_modifier.crowded",
}); });
} }
if (modifiers.isHardNations) {
result.push({
labelKey: "host_modal.hard_nations",
badgeKey: "public_game_modifier.hard_nations",
});
}
if (modifiers.startingGold) { if (modifiers.startingGold) {
result.push({ result.push({
labelKey: "host_modal.starting_gold", labelKey: "host_modal.starting_gold",
badgeKey: "public_game_modifier.starting_gold", badgeKey: "public_game_modifier.starting_gold",
badgeParams: {
amount: Math.round(modifiers.startingGold / 1_000_000),
},
value: modifiers.startingGold, value: modifiers.startingGold,
}); });
} }
@@ -155,7 +166,9 @@ export function getActiveModifiers(
export function getModifierLabels( export function getModifierLabels(
modifiers: PublicGameModifiers | undefined, modifiers: PublicGameModifiers | undefined,
): string[] { ): string[] {
return getActiveModifiers(modifiers).map((m) => translateText(m.badgeKey)); return getActiveModifiers(modifiers).map((m) =>
translateText(m.badgeKey, m.badgeParams),
);
} }
export function renderDuration(totalSeconds: number): string { export function renderDuration(totalSeconds: number): string {
+1
View File
@@ -215,6 +215,7 @@ export const GameConfigSchema = z.object({
isCompact: z.boolean(), isCompact: z.boolean(),
isRandomSpawn: z.boolean(), isRandomSpawn: z.boolean(),
isCrowded: z.boolean(), isCrowded: z.boolean(),
isHardNations: z.boolean(),
startingGold: z.number().int().min(0).optional(), startingGold: z.number().int().min(0).optional(),
}) })
.optional(), .optional(),
+1
View File
@@ -236,6 +236,7 @@ export interface PublicGameModifiers {
isCompact: boolean; isCompact: boolean;
isRandomSpawn: boolean; isRandomSpawn: boolean;
isCrowded: boolean; isCrowded: boolean;
isHardNations: boolean;
startingGold?: number; startingGold?: number;
} }
+174 -40
View File
@@ -89,16 +89,33 @@ const TEAM_WEIGHTS: { config: TeamCountConfig; weight: number }[] = [
{ config: HumansVsNations, weight: 20 }, { config: HumansVsNations, weight: 20 },
]; ];
type ModifierKey = "isRandomSpawn" | "isCompact" | "isCrowded" | "startingGold"; type ModifierKey =
| "isRandomSpawn"
| "isCompact"
| "isCrowded"
| "isHardNations"
| "startingGold"
| "startingGoldHigh";
// Each entry represents one "ticket" in the pool. More tickets = higher chance of selection. // Each entry represents one "ticket" in the pool. More tickets = higher chance of selection.
const SPECIAL_MODIFIER_POOL: ModifierKey[] = [ const SPECIAL_MODIFIER_POOL: ModifierKey[] = [
...Array<ModifierKey>(4).fill("isRandomSpawn"), ...Array<ModifierKey>(4).fill("isRandomSpawn"),
...Array<ModifierKey>(7).fill("isCompact"), ...Array<ModifierKey>(8).fill("isCompact"),
...Array<ModifierKey>(1).fill("isCrowded"), ...Array<ModifierKey>(1).fill("isCrowded"),
...Array<ModifierKey>(6).fill("startingGold"), ...Array<ModifierKey>(1).fill("isHardNations"),
...Array<ModifierKey>(8).fill("startingGold"),
...Array<ModifierKey>(1).fill("startingGoldHigh"),
]; ];
// Modifiers that cannot be active at the same time.
const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [
["startingGold", "startingGoldHigh"],
["isHardNations", "startingGoldHigh"],
];
// Probability of hard nations modifier in HumansVsNations games.
const HARD_NATIONS_HVN_PROBABILITY = 0.2; // 20%
export class MapPlaylist { export class MapPlaylist {
private playlists: Record<PublicGameType, GameMapType[]> = { private playlists: Record<PublicGameType, GameMapType[]> = {
ffa: [], ffa: [],
@@ -111,17 +128,15 @@ export class MapPlaylist {
return this.getSpecialConfig(); return this.getSpecialConfig();
} }
// TODO: consider moving modifier to special lobby.
const mode = type === "ffa" ? GameMode.FFA : GameMode.Team; const mode = type === "ffa" ? GameMode.FFA : GameMode.Team;
const map = this.getNextMap(type); const map = this.getNextMap(type);
const playerTeams = const playerTeams =
mode === GameMode.Team ? this.getTeamCount() : undefined; mode === GameMode.Team ? this.getTeamCount() : undefined;
const modifiers = this.getRandomPublicGameModifiers(); const modifiers = this.getRandomPublicGameModifiers(playerTeams);
const { startingGold } = modifiers; const { startingGold } = modifiers;
let { isCompact, isRandomSpawn, isCrowded } = modifiers; let { isCompact, isRandomSpawn, isCrowded, isHardNations } = modifiers;
// Duos, Trios, and Quads should not get random spawn (as it defeats the purpose) // Duos, Trios, and Quads should not get random spawn (as it defeats the purpose)
if ( if (
@@ -132,11 +147,16 @@ export class MapPlaylist {
isRandomSpawn = false; isRandomSpawn = false;
} }
// Maps with smallest player count (third number of calculateMapPlayerCounts) < 50 don't support compact map in team games // Hard nations modifier only applies when nations are present
// (not enough players after 75% player reduction for compact maps) if (mode === GameMode.Team && playerTeams !== HumansVsNations) {
isHardNations = false;
}
// Check if compact map would leave every team with at least 2 players
if ( if (
isCompact &&
mode === GameMode.Team && mode === GameMode.Team &&
!(await this.supportsCompactMapForTeams(map)) !(await this.supportsCompactMapForTeams(map, playerTeams!))
) { ) {
isCompact = false; isCompact = false;
} }
@@ -167,10 +187,11 @@ export class MapPlaylist {
isCompact, isCompact,
isRandomSpawn, isRandomSpawn,
isCrowded, isCrowded,
isHardNations,
startingGold, startingGold,
}, },
startingGold, startingGold,
difficulty: Difficulty.Medium, difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium,
infiniteGold: false, infiniteGold: false,
infiniteTroops: false, infiniteTroops: false,
maxTimerValue: undefined, maxTimerValue: undefined,
@@ -180,7 +201,10 @@ export class MapPlaylist {
gameMode: mode, gameMode: mode,
playerTeams, playerTeams,
bots: isCompact ? 100 : 400, bots: isCompact ? 100 : 400,
spawnImmunityDuration: startingGold ? 30 * 10 : 5 * 10, spawnImmunityDuration: this.getSpawnImmunityDuration(
playerTeams,
startingGold,
),
disabledUnits: [], disabledUnits: [],
} satisfies GameConfig; } satisfies GameConfig;
} }
@@ -190,12 +214,16 @@ export class MapPlaylist {
const map = this.getNextMap("special"); const map = this.getNextMap("special");
const playerTeams = const playerTeams =
mode === GameMode.Team ? this.getTeamCount() : undefined; mode === GameMode.Team ? this.getTeamCount() : undefined;
const supportsCompact =
mode !== GameMode.Team || (await this.supportsCompactMapForTeams(map));
const excludedModifiers: ModifierKey[] = []; const excludedModifiers: ModifierKey[] = [];
const supportsCompact =
mode !== GameMode.Team ||
(await this.supportsCompactMapForTeams(map, playerTeams!));
if (!supportsCompact) { if (!supportsCompact) {
excludedModifiers.push("isCompact"); excludedModifiers.push("isCompact");
} }
if ( if (
playerTeams === Duos || playerTeams === Duos ||
playerTeams === Trios || playerTeams === Trios ||
@@ -204,8 +232,29 @@ export class MapPlaylist {
excludedModifiers.push("isRandomSpawn"); excludedModifiers.push("isRandomSpawn");
} }
let { isCrowded, startingGold, isCompact, isRandomSpawn } = // Hard nations: excluded for non-HvN team modes (no nations present).
this.getRandomSpecialGameModifiers(excludedModifiers); // For HumansVsNations: rolled independently (not via pool).
// For FFA: stays in the pool for normal ticket-based selection.
let hardNationsFromIndependentRoll: boolean | undefined;
let poolCountReduction = 0;
if (mode === GameMode.Team && playerTeams !== HumansVsNations) {
excludedModifiers.push("isHardNations");
} else if (playerTeams === HumansVsNations) {
excludedModifiers.push("isHardNations");
excludedModifiers.push("startingGoldHigh"); // Nations are disabled if that modifier is active
hardNationsFromIndependentRoll =
Math.random() < HARD_NATIONS_HVN_PROBABILITY;
poolCountReduction = hardNationsFromIndependentRoll ? 1 : 0;
}
const poolResult = this.getRandomSpecialGameModifiers(
excludedModifiers,
undefined,
poolCountReduction,
);
let { isCrowded, startingGold, isCompact, isRandomSpawn } = poolResult;
let isHardNations =
hardNationsFromIndependentRoll ?? poolResult.isHardNations;
let crowdedMaxPlayers: number | undefined; let crowdedMaxPlayers: number | undefined;
if (isCrowded) { if (isCrowded) {
@@ -216,10 +265,21 @@ export class MapPlaylist {
// Map doesn't support crowded. Drop it and pick one replacement only // Map doesn't support crowded. Drop it and pick one replacement only
// if it was the sole modifier, so the lobby always has at least one. // if it was the sole modifier, so the lobby always has at least one.
isCrowded = false; isCrowded = false;
if (!isRandomSpawn && !isCompact && startingGold === undefined) { if (
!isRandomSpawn &&
!isCompact &&
!isHardNations &&
startingGold === undefined
) {
excludedModifiers.push("isCrowded"); excludedModifiers.push("isCrowded");
({ isRandomSpawn, isCompact, startingGold } = const fallback = this.getRandomSpecialGameModifiers(
this.getRandomSpecialGameModifiers(excludedModifiers, 1)); excludedModifiers,
1,
poolCountReduction,
);
({ isRandomSpawn, isCompact, startingGold } = fallback);
isHardNations =
hardNationsFromIndependentRoll ?? fallback.isHardNations;
} }
} }
} }
@@ -230,6 +290,11 @@ export class MapPlaylist {
(await this.lobbyMaxPlayers(map, mode, playerTeams, isCompact)), (await this.lobbyMaxPlayers(map, mode, playerTeams, isCompact)),
); );
const disableNations =
(mode === GameMode.Team && playerTeams !== HumansVsNations) ||
// Nations don't have PVP immunity, so 25M starting gold wouldn't work well with them
(startingGold !== undefined && startingGold >= 25_000_000);
return { return {
donateGold: mode === GameMode.Team, donateGold: mode === GameMode.Team,
donateTroops: mode === GameMode.Team, donateTroops: mode === GameMode.Team,
@@ -241,20 +306,24 @@ export class MapPlaylist {
isCompact, isCompact,
isRandomSpawn, isRandomSpawn,
isCrowded, isCrowded,
isHardNations,
startingGold, startingGold,
}, },
startingGold, startingGold,
difficulty: Difficulty.Medium, difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium,
infiniteGold: false, infiniteGold: false,
infiniteTroops: false, infiniteTroops: false,
maxTimerValue: undefined, maxTimerValue: undefined,
instantBuild: false, instantBuild: false,
randomSpawn: isRandomSpawn, randomSpawn: isRandomSpawn,
disableNations: mode === GameMode.Team && playerTeams !== HumansVsNations, disableNations,
gameMode: mode, gameMode: mode,
playerTeams, playerTeams,
bots: isCompact ? 100 : 400, bots: isCompact ? 100 : 400,
spawnImmunityDuration: startingGold ? 30 * 10 : 5 * 10, spawnImmunityDuration: this.getSpawnImmunityDuration(
playerTeams,
startingGold,
),
disabledUnits: [], disabledUnits: [],
} satisfies GameConfig; } satisfies GameConfig;
} }
@@ -375,30 +444,39 @@ export class MapPlaylist {
return TEAM_WEIGHTS[0].config; return TEAM_WEIGHTS[0].config;
} }
private getRandomPublicGameModifiers(): PublicGameModifiers { private getRandomPublicGameModifiers(
playerTeams?: TeamCountConfig,
): PublicGameModifiers {
return { return {
isRandomSpawn: Math.random() < 0.1, // 10% chance isRandomSpawn: Math.random() < 0.05, // 5% chance
isCompact: Math.random() < 0.05, // 5% chance isCompact: Math.random() < 0.05, // 5% chance
isCrowded: Math.random() < 0.05, // 5% chance isCrowded: Math.random() < 0.05, // 5% chance
startingGold: Math.random() < 0.05 ? 5_000_000 : undefined, // 5% chance startingGold: Math.random() < 0.05 ? 5_000_000 : undefined, // 5% chance
isHardNations:
playerTeams === HumansVsNations
? Math.random() < HARD_NATIONS_HVN_PROBABILITY
: Math.random() < 0.025, // 2.5% chance
}; };
} }
private getRandomSpecialGameModifiers( private getRandomSpecialGameModifiers(
excludedModifiers: ModifierKey[] = [], excludedModifiers: ModifierKey[] = [],
count?: number, count?: number,
countReduction: number = 0,
): PublicGameModifiers { ): PublicGameModifiers {
// Roll how many modifiers to pick: 30% → 1, 40% → 2, 20% → 3, 10% → 4 // Roll how many modifiers to pick: 30% → 1, 40% → 2, 20% → 3, 10% → 4
const modifierCountRoll = Math.floor(Math.random() * 10) + 1; const modifierCountRoll = Math.floor(Math.random() * 10) + 1;
const k = const k = Math.max(
count ?? 0,
(modifierCountRoll <= 3 (count ??
? 1 (modifierCountRoll <= 3
: modifierCountRoll <= 7 ? 1
? 2 : modifierCountRoll <= 7
: modifierCountRoll <= 9 ? 2
? 3 : modifierCountRoll <= 9
: 4); ? 3
: 4)) - countReduction,
);
// Shuffle the pool, then pick the first k unique modifier keys. // Shuffle the pool, then pick the first k unique modifier keys.
const pool = SPECIAL_MODIFIER_POOL.filter( const pool = SPECIAL_MODIFIER_POOL.filter(
@@ -408,23 +486,79 @@ export class MapPlaylist {
const selected = new Set<ModifierKey>(); const selected = new Set<ModifierKey>();
for (const key of pool) { for (const key of pool) {
if (selected.size >= k) break; if (selected.size >= k) break;
selected.add(key); // Skip if a mutually exclusive modifier is already selected
const blocked = MUTUALLY_EXCLUSIVE_MODIFIERS.some(
([a, b]) =>
(key === a && selected.has(b)) || (key === b && selected.has(a)),
);
if (!blocked) selected.add(key);
} }
return { return {
isRandomSpawn: selected.has("isRandomSpawn"), isRandomSpawn: selected.has("isRandomSpawn"),
isCompact: selected.has("isCompact"), isCompact: selected.has("isCompact"),
isCrowded: selected.has("isCrowded"), isCrowded: selected.has("isCrowded"),
startingGold: selected.has("startingGold") ? 5_000_000 : undefined, isHardNations: selected.has("isHardNations"),
startingGold: selected.has("startingGoldHigh")
? 25_000_000
: selected.has("startingGold")
? 5_000_000
: undefined,
}; };
} }
// Maps with smallest player count (third number of calculateMapPlayerCounts) < 50 don't support compact map in team games // Check whether a compact map still gives every team at least 2 players,
// (not enough players after 75% player reduction for compact maps) // using the worst-case player tier (smallest) from lobbyMaxPlayers.
private async supportsCompactMapForTeams(map: GameMapType): Promise<boolean> { private async supportsCompactMapForTeams(
map: GameMapType,
playerTeams: TeamCountConfig,
): Promise<boolean> {
const landTiles = await getMapLandTiles(map); const landTiles = await getMapLandTiles(map);
const [, , smallest] = this.calculateMapPlayerCounts(landTiles); const [l, , s] = this.calculateMapPlayerCounts(landTiles);
return smallest >= 50; // Worst case: smallest tier with team mode 1.5x multiplier, capped at l
let p = Math.min(Math.ceil(s * 1.5), l);
// Apply compact 75% player reduction
p = Math.max(3, Math.floor(p * 0.25));
// Apply team adjustment
p = this.adjustForTeams(p, playerTeams);
// Check at least 2 players per team
return this.playersPerTeam(p, playerTeams) >= 2;
}
private playersPerTeam(
adjustedPlayerCount: number,
playerTeams: TeamCountConfig,
): number {
switch (playerTeams) {
case Duos:
return Math.min(2, adjustedPlayerCount);
case Trios:
return Math.min(3, adjustedPlayerCount);
case Quads:
return Math.min(4, adjustedPlayerCount);
case HumansVsNations:
return adjustedPlayerCount; // adjustedPlayerCount is the human count
default:
return Math.floor(adjustedPlayerCount / playerTeams);
}
}
/**
* Centralised spawn-immunity duration logic.
* - HumansVsNations: always 5s (nations can't benefit from longer PVP immunity)
* - 25M starting gold: 2:30 (extra time to compensate for high gold)
* - 5M starting gold: 30s
* - Default: 5s
*/
private getSpawnImmunityDuration(
playerTeams?: TeamCountConfig,
startingGold?: number,
): number {
if (playerTeams === HumansVsNations) return 5 * 10;
if (startingGold !== undefined && startingGold >= 25_000_000)
return 150 * 10;
if (startingGold) return 30 * 10;
return 5 * 10;
} }
private async getCrowdedMaxPlayers( private async getCrowdedMaxPlayers(
+6 -1
View File
@@ -80,7 +80,12 @@ export class TestServerConfig implements ServerConfig {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
getRandomPublicGameModifiers(): PublicGameModifiers { getRandomPublicGameModifiers(): PublicGameModifiers {
return { isCompact: false, isRandomSpawn: false, isCrowded: false }; return {
isCompact: false,
isRandomSpawn: false,
isCrowded: false,
isHardNations: false,
};
} }
async supportsCompactMapForTeams(): Promise<boolean> { async supportsCompactMapForTeams(): Promise<boolean> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");