todo: QUAL LEADERBAORDS AND SORTING
This commit is contained in:
parent
e9d0305566
commit
02c03e3751
12 changed files with 2581 additions and 108 deletions
|
|
@ -5,6 +5,7 @@
|
|||
import { TAServerPort, TAServerUrl, authTokenStore, client } from "$lib/stores";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { onMount } from "svelte";
|
||||
import type { BeatSaverMap } from "$lib/services/beatsaver";
|
||||
|
||||
export let tournamentGuid: string;
|
||||
export let poolGuid: string;
|
||||
|
|
@ -53,33 +54,6 @@
|
|||
|
||||
// Playing-specific settings
|
||||
let disableCustomNotesOnStream = false;
|
||||
|
||||
type BeatSaverMap = {
|
||||
id: string;
|
||||
metadata: {
|
||||
songName: string;
|
||||
songAuthorName: string;
|
||||
levelAuthorName: string;
|
||||
songSubName: string;
|
||||
duration: number;
|
||||
bpm: number;
|
||||
};
|
||||
versions: Array<{
|
||||
hash: string;
|
||||
state: string;
|
||||
createdAt: string;
|
||||
downloadURL: string;
|
||||
diffs: Array<{
|
||||
characteristic: string;
|
||||
difficulty: string;
|
||||
maxScore: number;
|
||||
nps: number;
|
||||
}>;
|
||||
coverURL: string;
|
||||
previewURL: string;
|
||||
}>;
|
||||
uploaded: string;
|
||||
};
|
||||
|
||||
// Beat Saber Modifiers with incompatibility groups
|
||||
interface Modifier {
|
||||
|
|
@ -433,7 +407,7 @@
|
|||
selectedCharacteristic = availableCharacteristics[0] || "";
|
||||
selectedDifficulty = availableDifficulties[selectedCharacteristic]?.[0] || "";
|
||||
|
||||
mapBeatmapId = `custom_level_${latestVersion.hash}`;
|
||||
mapBeatmapId = `custom_level_${latestVersion.hash.toLocaleUpperCase()}`;
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
|
|
@ -510,20 +484,25 @@
|
|||
disableScoresaberSubmission: mode === 'playing' ? disableScoresaberSubmission : false,
|
||||
showScoreboard: mode === 'playing' ? showScoreboard : false,
|
||||
useSync: false,
|
||||
target: 0
|
||||
target: 0,
|
||||
playerSettings: {
|
||||
options: 0,
|
||||
playerHeight: 0,
|
||||
sfxVolume: 0,
|
||||
saberTrailIntensity: 0,
|
||||
noteJumpStartBeatOffset: 0,
|
||||
noteJumpFixedDuration: 0,
|
||||
noteJumpDurationTypeSettings: 0,
|
||||
arcVisibilityType: 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
if (map) {
|
||||
// I will need to fix this, for now this is a TA server issue
|
||||
dispatch('mapUpdated', { map: mapData });
|
||||
} else {
|
||||
await client.addTournamentPoolMaps(tournamentGuid, poolGuid, [mapData]);
|
||||
dispatch('mapAdded', { map: mapData });
|
||||
}
|
||||
|
||||
// if (response.type !== Response_ResponseType.Success) {
|
||||
// throw new Error("Failed to save map");
|
||||
// }
|
||||
|
||||
isLoading = false;
|
||||
return dispatch('close');
|
||||
} catch (err) {
|
||||
|
|
@ -747,19 +726,7 @@
|
|||
<h4>Tournament Assistant Settings</h4>
|
||||
<p class="section-desc">Configure game behavior options</p>
|
||||
|
||||
<div class="qualifier-options">
|
||||
<div class="qualifier-toggle">
|
||||
<label class="switch-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={disableFail}
|
||||
on:change={handleDisableFailChange}
|
||||
/>
|
||||
<span class="switch"></span>
|
||||
<span class="label-text">Disable Fail</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="qualifier-options">
|
||||
<div class="qualifier-toggle">
|
||||
<label class="switch-label">
|
||||
<input
|
||||
|
|
@ -772,6 +739,18 @@
|
|||
</div>
|
||||
|
||||
{#if mode === 'playing'}
|
||||
<div class="qualifier-toggle">
|
||||
<label class="switch-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={disableFail}
|
||||
on:change={handleDisableFailChange}
|
||||
/>
|
||||
<span class="switch"></span>
|
||||
<span class="label-text">Disable Fail</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="qualifier-toggle">
|
||||
<label class="switch-label">
|
||||
<input
|
||||
|
|
@ -816,7 +795,6 @@
|
|||
<span class="switch"></span>
|
||||
<span class="label-text">Limit Attempts</span>
|
||||
</label>
|
||||
<p class="setting-description">Restrict the number of attempts allowed for this qualifier</p>
|
||||
</div>
|
||||
|
||||
{#if limitAttemptsEnabled}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
interface BeatSaverMap {
|
||||
export interface BeatSaverMap {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
@ -58,6 +58,7 @@ interface BeatSaverMap {
|
|||
}>;
|
||||
downloadURL: string;
|
||||
previewURL: string;
|
||||
coverURL: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +118,22 @@ export async function fetchMapsByHashesOrKeys(identifiers: string[]): Promise<Be
|
|||
}
|
||||
|
||||
// Process hashes in batches
|
||||
if (hashes.length > 0) {
|
||||
if (hashes.length == 1) {
|
||||
try {
|
||||
const response = await fetch(`${BEATSAVER_API_BASE}/maps/hash/${hashes[0]}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`BeatSaver API error for hashes: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const maps = [data];
|
||||
allMaps.push(...maps);
|
||||
} catch (error) {
|
||||
console.error('Error fetching hash batch:', error);
|
||||
throw error;
|
||||
}
|
||||
} else if (hashes.length > 0) {
|
||||
const hashBatches: string[][] = [];
|
||||
for (let i = 0; i < hashes.length; i += BATCH_SIZE) {
|
||||
hashBatches.push(hashes.slice(i, i + BATCH_SIZE));
|
||||
|
|
@ -126,7 +142,7 @@ export async function fetchMapsByHashesOrKeys(identifiers: string[]): Promise<Be
|
|||
for (const batch of hashBatches) {
|
||||
try {
|
||||
const hashesParam = batch.join(',');
|
||||
const response = await fetch(`${BEATSAVER_API_BASE}/maps/hashes/${hashesParam}`);
|
||||
const response = await fetch(`${BEATSAVER_API_BASE}/maps/hash/${hashesParam}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`BeatSaver API error for hashes: ${response.status} ${response.statusText}`);
|
||||
|
|
|
|||
|
|
@ -26,5 +26,7 @@ export const TAServerUrl = createPersistedStore('TAServerUrl', "server.tournamen
|
|||
|
||||
export const TAServerPort = createPersistedStore('TAServerPort', "8676");
|
||||
|
||||
export const TAServerPlayerPort = createPersistedStore('TAServerPlayerPort', "8675");
|
||||
|
||||
// In the future, use a store for TAClient, since svelte is neat :)))
|
||||
export const client = new TAClient();
|
||||
|
|
@ -134,11 +134,11 @@
|
|||
|
||||
<div class="footer-section contact">
|
||||
<h4>Contact</h4>
|
||||
<a href="mailto:support@tournamentassistant.net" class="contact-link">
|
||||
<a href="mailto:support@beatkhana.com" class="contact-link">
|
||||
<span class="material-icons">email</span>
|
||||
support@tournamentassistant.net
|
||||
support@beatkhana.com
|
||||
</a>
|
||||
<a href="https://discord.gg/tournament" class="contact-link">
|
||||
<a href="https://discord.gg/AnkmKk6AD8" class="contact-link">
|
||||
<span class="material-icons">forum</span>
|
||||
Join our Discord
|
||||
</a>
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
</div>
|
||||
<div class="footer-bottom">
|
||||
<div class="copyright">
|
||||
© {new Date().getFullYear()} Tournament Assistant. All rights reserved.
|
||||
© {new Date().getFullYear()} Luna & Moon. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
import Notification from '$lib/components/notifications/Popup.svelte';
|
||||
import { discordAuthUrl } from '$lib/config.json';
|
||||
import { discordDataStore, discordTokenStore, authTokenStore, TAServerPort, TAServerUrl, client } from '$lib/stores';
|
||||
import { discordDataStore, discordTokenStore, authTokenStore, TAServerPort, TAServerUrl, client, TAServerPlayerPort } from '$lib/stores';
|
||||
import { TAClient, Response_ResponseType, Tournament } from 'moons-ta-client';
|
||||
import { bufferToImageUrl } from '$lib/services/taImages';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
|
@ -125,13 +125,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// if (client.isConnected == true) {
|
||||
// // Properly disconnect and clean up any listeners
|
||||
// client.disconnect();
|
||||
// }
|
||||
});
|
||||
|
||||
async function fetchTournamentAuthorisedUsers(tournamentGuid: string) {
|
||||
// Get the authorised users for the tournament
|
||||
let authorisedUsers = [];
|
||||
|
|
@ -213,14 +206,7 @@
|
|||
try {
|
||||
creating = true;
|
||||
|
||||
// Connect to the TA server
|
||||
const connectResult = await client.connect('server.tournamentassistant.net', '8676');
|
||||
|
||||
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
|
||||
authError = connectResult.details.connect.message;
|
||||
creating = false;
|
||||
return;
|
||||
}
|
||||
if(!client.isConnected) return;
|
||||
|
||||
// Create the tournament object
|
||||
let tournament: Tournament = {
|
||||
|
|
@ -241,6 +227,12 @@
|
|||
pools: [],
|
||||
allowUnauthorizedView: true
|
||||
},
|
||||
server: {
|
||||
name: `${$TAServerUrl}:${$TAServerPort}`,
|
||||
address: $TAServerUrl,
|
||||
port: $TAServerPlayerPort,
|
||||
websocketPort: $TAServerPort
|
||||
}
|
||||
};
|
||||
|
||||
// Convert image to Uint8Array if an image was provided
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { onMount, onDestroy } from "svelte";
|
||||
import { bkAPIUrl } from '$lib/config.json';
|
||||
//@ts-ignore
|
||||
import { Match, Tournament, TAClient, Response_ResponseType } from 'moons-ta-client';
|
||||
import { Match, Tournament, TAClient, Response_ResponseType, User, User_PlayStates } from 'moons-ta-client';
|
||||
import { writable } from "svelte/store";
|
||||
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
|
@ -15,25 +15,10 @@
|
|||
let isLoading = false;
|
||||
let error: string | null = null;
|
||||
let matches: Match[] = [];
|
||||
let availablePlayers: any[] = [];
|
||||
let selectedPlayers: any[] = [];
|
||||
let isCreatingMatch: boolean = false;
|
||||
let availablePlayers: User[] = [];
|
||||
|
||||
// Store for selected player guids
|
||||
const selectedPlayerGuids = writable<string[]>([]);
|
||||
|
||||
// Player status enum
|
||||
enum PlayerStatus {
|
||||
Downloading = "downloading",
|
||||
SelectingSong = "selecting",
|
||||
Idle = "idle"
|
||||
}
|
||||
|
||||
enum PlayerPlayState {
|
||||
In_Menu = 0,
|
||||
Waiting_For_Coordinator = 1,
|
||||
In_Game = 2
|
||||
}
|
||||
|
||||
// Remove 'Bearer ' from the token
|
||||
if($authTokenStore) {
|
||||
|
|
@ -49,13 +34,13 @@
|
|||
}
|
||||
|
||||
// Return status color based on player status
|
||||
function getStatusColor(status: PlayerStatus): string {
|
||||
function getStatusColor(status: User_PlayStates): string {
|
||||
switch (status) {
|
||||
case PlayerStatus.Downloading:
|
||||
case User_PlayStates.InGame:
|
||||
return "var(--danger-color)";
|
||||
case PlayerStatus.SelectingSong:
|
||||
case User_PlayStates.InMenu:
|
||||
return "#FCD34D"; // Yellow
|
||||
case PlayerStatus.Idle:
|
||||
case User_PlayStates.WaitingForCoordinator:
|
||||
return "#10B981"; // Green
|
||||
default:
|
||||
return "var(--text-secondary)";
|
||||
|
|
@ -110,6 +95,7 @@
|
|||
tournament = client.stateManager.getTournament(tournamentGuid);
|
||||
if(!tournament?.users.some(user => user.guid == client.stateManager.getSelfGuid())) {
|
||||
const joinResult = await client.joinTournament(tournamentGuid);
|
||||
console.log('jointournament', joinResult);
|
||||
if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) {
|
||||
throw new Error('Could not join tournament');
|
||||
}
|
||||
|
|
@ -159,19 +145,46 @@
|
|||
associatedUsers: $selectedPlayerGuids
|
||||
});
|
||||
|
||||
console.log(response)
|
||||
console.log("Match created! Response: ", response);
|
||||
|
||||
const newMatch = (response as any).details.createMatch.match;
|
||||
|
||||
goto(`/tournaments/${tournamentGuid}/matches/${newMatch.guid}`);
|
||||
}
|
||||
|
||||
async function handleUserConnected(params: [User, Tournament]) {
|
||||
if(params[1].guid == tournament?.guid) {
|
||||
availablePlayers.push(params[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUserDisconnected(params: [User, Tournament]) {
|
||||
if(params[1].guid == tournament?.guid) {
|
||||
if(availablePlayers.some(x => x.guid == params[0].guid)) {
|
||||
availablePlayers = availablePlayers.filter(x => x.guid !== params[0].guid);
|
||||
} else {
|
||||
let newMatchObject = matches.find(x => x.associatedUsers.includes(params[0].guid))!;
|
||||
newMatchObject.associatedUsers = newMatchObject.associatedUsers.filter(x => x !== params[0].guid);
|
||||
matches = [
|
||||
...matches.filter(x => x.guid !== newMatchObject.guid),
|
||||
newMatchObject
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDiscordNameBecauseShit(user: User) {
|
||||
return user.discordInfo?.username || "";
|
||||
}
|
||||
|
||||
onMount(async() => {
|
||||
if ($authTokenStore) {
|
||||
await fetchTournamentData();
|
||||
client.stateManager.on('matchCreated', handleMatchCreated);
|
||||
client.stateManager.on('matchDeleted', handleMatchDeleted);
|
||||
client.stateManager.on('matchUpdated', handleMatchUpdated);
|
||||
client.stateManager.on('userConnected', handleUserConnected);
|
||||
client.stateManager.on('userDisconnected', handleUserDisconnected);
|
||||
} else {
|
||||
window.location.href = "/discordAuth"
|
||||
}
|
||||
|
|
@ -181,6 +194,8 @@
|
|||
client.stateManager.removeListener('matchUpdated', handleMatchUpdated);
|
||||
client.stateManager.removeListener('matchDeleted', handleMatchDeleted);
|
||||
client.stateManager.removeListener('matchCreated', handleMatchDeleted);
|
||||
client.stateManager.removeListener('userConnected', handleUserConnected);
|
||||
client.stateManager.removeListener('userDisconnected', handleUserDisconnected);
|
||||
client.removeListener('createdMatch', handleMatchUpdated)
|
||||
// client.disconnect();
|
||||
});
|
||||
|
|
@ -287,8 +302,8 @@
|
|||
alt="Player"
|
||||
class="player-avatar">
|
||||
<div class="player-info">
|
||||
<h4>{player.name} ({player.discordInfo.username})</h4>
|
||||
<p>{PlayerPlayState[player.playState]}</p>
|
||||
<h4>{player.name} ({getDiscordNameBecauseShit(player)})</h4>
|
||||
<p>{User_PlayStates[player.playState]}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -149,16 +149,22 @@
|
|||
}
|
||||
|
||||
async function handleMapUpdated(event: CustomEvent) {
|
||||
const res = await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
|
||||
console.log(res)
|
||||
await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
|
||||
fetchMapPoolData();
|
||||
showSuccessNotification = true;
|
||||
setTimeout(() => {
|
||||
showSuccessNotification = false;
|
||||
}, 2500);
|
||||
successMessage = "Map successfully updated!";
|
||||
}
|
||||
|
||||
function handleMapAdded(event: CustomEvent) {
|
||||
async function handleMapAdded(event: CustomEvent) {
|
||||
await client.addTournamentPoolMaps(tournamentGuid, poolGuid, [event.detail.map]);
|
||||
fetchMapPoolData();
|
||||
showSuccessNotification = true;
|
||||
setTimeout(() => {
|
||||
showSuccessNotification = false;
|
||||
}, 2500);
|
||||
successMessage = "Map successfully added!";
|
||||
}
|
||||
|
||||
|
|
@ -260,25 +266,25 @@
|
|||
}
|
||||
|
||||
const response: BeatSaverMap = await data.json();
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function getActiveModifiersCompact(gameplayModifiers: number): string[] {
|
||||
const activeModifiers: string[] = [];
|
||||
|
||||
// Get all numeric enum values and sort them
|
||||
// Get all numeric enum values (powers of 2) and sort them
|
||||
const values = Object.values(GameplayModifiers_GameOptions)
|
||||
.filter((value): value is number =>
|
||||
typeof value === 'number' &&
|
||||
value !== GameplayModifiers_GameOptions.None
|
||||
value !== GameplayModifiers_GameOptions.None &&
|
||||
value > 0
|
||||
)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
console.log(values)
|
||||
|
||||
for (const value of values) {
|
||||
if (gameplayModifiers & value) {
|
||||
// Use bitwise AND to check if this specific flag is set
|
||||
if ((gameplayModifiers & value) === value) {
|
||||
const name = modifierNameMap[value];
|
||||
if (name && name.trim() !== "") {
|
||||
activeModifiers.push(name);
|
||||
|
|
@ -290,11 +296,11 @@
|
|||
}
|
||||
|
||||
function getModifierArray(map: any): string[] {
|
||||
if (map.taData.gameplayParameters?.gameplayModifiers.options == 0) {
|
||||
if (map.taData.gameplayParameters?.gameplayModifiers?.options == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getActiveModifiersCompact(map.taData.gameplayParameters.gameplayModifiers);
|
||||
return getActiveModifiersCompact(map.taData.gameplayParameters?.gameplayModifiers?.options || 0);
|
||||
}
|
||||
|
||||
onMount(async() => {
|
||||
|
|
@ -414,6 +420,18 @@
|
|||
{:else}
|
||||
<span class="no-modifiers">No modifiers active</span>
|
||||
{/each}
|
||||
{#if map.taData.gameplayParameters?.disableFail}
|
||||
<span class="modifier-tag">Disable Fail</span>
|
||||
{/if}
|
||||
{#if map.taData.gameplayParameters?.disableCustomNotesOnStream}
|
||||
<span class="modifier-tag">Disable Custom Notes On Stream</span>
|
||||
{/if}
|
||||
{#if map.taData.gameplayParameters?.disablePause}
|
||||
<span class="modifier-tag">Disable Pause</span>
|
||||
{/if}
|
||||
{#if map.taData.gameplayParameters?.disableScoresaberSubmission}
|
||||
<span class="modifier-tag">Disable ScoreSaber Submission</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -258,7 +258,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// API functions - implement these based on your TA client
|
||||
async function kickPlayerFromMatch(playerGuid: string) {
|
||||
const response = await client.removeUserFromMatch(tournamentGuid, matchGuid, playerGuid);
|
||||
console.log('Kicking player:', playerGuid);
|
||||
|
|
@ -280,6 +279,13 @@
|
|||
console.log('Sending players back to menu');
|
||||
}
|
||||
|
||||
async function handleUserDisconnected(params: [User, Tournament]) {
|
||||
if(matchPlayers.some(user => user.guid == params[0].guid)) {
|
||||
console.log("User disconnected from match. User: ", params[0])
|
||||
matchPlayers = matchPlayers.filter(x => x.guid !== params[0].guid);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async() => {
|
||||
if ($authTokenStore) {
|
||||
// Fetch the match data that we need to display
|
||||
|
|
@ -287,6 +293,7 @@
|
|||
// Using stateManger listen to global changes to our match
|
||||
client.stateManager.on('matchDeleted', handleMatchDeleted);
|
||||
client.stateManager.on('matchUpdated', handleMatchUpdated);
|
||||
client.stateManager.on('userDisconnected', handleUserDisconnected)
|
||||
} else {
|
||||
window.location.href = "/discordAuth"
|
||||
}
|
||||
|
|
@ -296,6 +303,7 @@
|
|||
// Remove the listeners that were added on mount
|
||||
client.stateManager.removeListener('matchUpdated', handleMatchUpdated);
|
||||
client.stateManager.removeListener('matchDeleted', handleMatchDeleted);
|
||||
client.stateManager.removeListener('userDisconnected', handleUserDisconnected);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
1100
src/routes/tournaments/[tournamentguid]/qualifiers/+page.svelte
Normal file
1100
src/routes/tournaments/[tournamentguid]/qualifiers/+page.svelte
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,6 @@
|
|||
export function load({ params }: any){
|
||||
return {
|
||||
tournamentGuid: params.tournamentguid,
|
||||
params: params
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,7 @@
|
|||
export function load({ params }: any){
|
||||
return {
|
||||
tournamentGuid: params.tournamentguid,
|
||||
qualifierGuid: params.qualifierGuid,
|
||||
params: params
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue