we do be staging
This commit is contained in:
parent
49e1da1cbd
commit
3da3624df0
11 changed files with 3036 additions and 326 deletions
|
|
@ -2,42 +2,12 @@
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { type Map, GameplayModifiers_GameOptions, Push_SongFinished, RealtimeScore } from 'moons-ta-client';
|
import { type Map, GameplayModifiers_GameOptions, Push_SongFinished, RealtimeScore } from 'moons-ta-client';
|
||||||
import type { BeatSaverMap } from '$lib/services/beatsaver.js';
|
import type { BeatSaverMap } from '$lib/services/beatsaver.js';
|
||||||
|
import { type ScoreWithAccuracy, type PreviousResults, MapDifficulty } from '$lib/interfaces/match.interfaces';
|
||||||
interface ScoreWithAccuracy extends Push_SongFinished {
|
|
||||||
accuracy: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PreviousResults {
|
|
||||||
taData: Map;
|
|
||||||
beatsaverData: BeatSaverMap;
|
|
||||||
scores: ScoreWithAccuracy[];
|
|
||||||
completionType: 'Completed' | 'Still Awaiting Scores' | 'Exited To Menu';
|
|
||||||
}
|
|
||||||
|
|
||||||
export let maps: PreviousResults[] = [];
|
export let maps: PreviousResults[] = [];
|
||||||
|
|
||||||
$: maps = maps.map(map => {
|
|
||||||
const newScores = map.scores.map(score => {
|
|
||||||
const accuracy = calculateAccuracy(score, map);
|
|
||||||
return {
|
|
||||||
...score,
|
|
||||||
accuracy: parseFloat(accuracy)
|
|
||||||
} as ScoreWithAccuracy;
|
|
||||||
});
|
|
||||||
map.scores = newScores;
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
enum MapDifficulty {
|
|
||||||
"Easy" = 0,
|
|
||||||
"Normal" = 1,
|
|
||||||
"Hard" = 2,
|
|
||||||
"Expert" = 3,
|
|
||||||
"ExpertPlus" = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifierNameMap: Record<number, string> = {
|
const modifierNameMap: Record<number, string> = {
|
||||||
[GameplayModifiers_GameOptions.None]: "",
|
[GameplayModifiers_GameOptions.None]: "",
|
||||||
[GameplayModifiers_GameOptions.NoFail]: "No Fail",
|
[GameplayModifiers_GameOptions.NoFail]: "No Fail",
|
||||||
|
|
@ -87,7 +57,7 @@
|
||||||
|
|
||||||
discordMessage += `**${positionText} ${displayName}**\n`;
|
discordMessage += `**${positionText} ${displayName}**\n`;
|
||||||
discordMessage += `Score: ${score.score.toLocaleString()}\n`;
|
discordMessage += `Score: ${score.score.toLocaleString()}\n`;
|
||||||
discordMessage += `Accuracy: ${score.accuracy.toFixed(2)}%\n`;
|
discordMessage += `Accuracy: ${score.accuracy}%\n`;
|
||||||
discordMessage += `Misses: ${score.misses} | Bad Cuts: ${score.badCuts}\n\n`;
|
discordMessage += `Misses: ${score.misses} | Bad Cuts: ${score.badCuts}\n\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -162,32 +132,6 @@
|
||||||
return 'help';
|
return 'help';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMaxScore(songInfo: BeatSaverMap, characteristic: string = "standard", difficulty: string): number {
|
|
||||||
const diff = maps.find(x => x.beatsaverData.versions[0].hash == songInfo.versions[0].hash)?.beatsaverData.versions[0].diffs.find(
|
|
||||||
(x) => ((characteristic.toLowerCase() !== 'expertplus' && characteristic.toLowerCase() !== 'expert+') ? x.characteristic.toLowerCase() === characteristic.toLowerCase() : (x.characteristic.toLowerCase() == 'expertplus' || x.characteristic.toLowerCase() == 'expert+')) && x.difficulty.toLowerCase() === difficulty.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
return diff?.maxScore ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to convert difficulty number to string
|
|
||||||
function getDifficultyAsString(difficulty: number): string {
|
|
||||||
return MapDifficulty[difficulty] || "ExpertPlus";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main accuracy calculation logic
|
|
||||||
function calculateAccuracy(score: Push_SongFinished, mapWithSongInfo: PreviousResults): string {
|
|
||||||
console.log(maps)
|
|
||||||
const maxScore = getMaxScore(
|
|
||||||
mapWithSongInfo.beatsaverData,
|
|
||||||
score.beatmap?.characteristic?.serializedName ?? "Standard",
|
|
||||||
getDifficultyAsString(score.beatmap?.difficulty ?? 4) || "ExpertPlus"
|
|
||||||
);
|
|
||||||
|
|
||||||
const accuracy = ((score.score / maxScore) * 100).toFixed(2);
|
|
||||||
return accuracy;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="previously-played">
|
<div class="previously-played">
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@
|
||||||
dispatch('removeMap', { map: map });
|
dispatch('removeMap', { map: map });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAddMapsFromTAPool() {
|
||||||
|
dispatch('addMapsFromTAPool');
|
||||||
|
}
|
||||||
|
|
||||||
function handleLoadMap(map: CustomMap) {
|
function handleLoadMap(map: CustomMap) {
|
||||||
mapToLoad = map;
|
mapToLoad = map;
|
||||||
showConfirmDialog = true;
|
showConfirmDialog = true;
|
||||||
|
|
@ -121,6 +125,10 @@
|
||||||
<span class="material-icons">add</span>
|
<span class="material-icons">add</span>
|
||||||
Add Map
|
Add Map
|
||||||
</button>
|
</button>
|
||||||
|
<button class="action-button add-map" on:click={handleAddMapsFromTAPool}>
|
||||||
|
<span class="material-icons">add</span>
|
||||||
|
Add Maps From TA Pool
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if maps.length === 0}
|
{#if maps.length === 0}
|
||||||
|
|
@ -337,8 +345,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
max-height: 60vh;
|
/* max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-card {
|
.map-card {
|
||||||
|
|
@ -608,7 +616,7 @@
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar Styling */
|
/* Scrollbar Styling
|
||||||
.maps-list::-webkit-scrollbar {
|
.maps-list::-webkit-scrollbar {
|
||||||
width: 0.375rem;
|
width: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
@ -625,7 +633,7 @@
|
||||||
|
|
||||||
.maps-list::-webkit-scrollbar-thumb:hover {
|
.maps-list::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--text-secondary);
|
background: var(--text-secondary);
|
||||||
}
|
} */
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
|
||||||
1054
src/lib/components/popups/AddMapsFromFile.svelte
Normal file
1054
src/lib/components/popups/AddMapsFromFile.svelte
Normal file
File diff suppressed because it is too large
Load diff
657
src/lib/components/popups/AddMapsFromTAPool.svelte
Normal file
657
src/lib/components/popups/AddMapsFromTAPool.svelte
Normal file
|
|
@ -0,0 +1,657 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import type { CustomTAMapPool, CustomMap } from '$lib/interfaces/match.interfaces';
|
||||||
|
import { bufferToImageUrl } from '$lib/services/taImages';
|
||||||
|
|
||||||
|
export let isOpen = false;
|
||||||
|
export let mapPools: CustomTAMapPool[] = [];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
// Track which pools are expanded
|
||||||
|
let expandedPools: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Track selected maps by their guid
|
||||||
|
let selectedMaps: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Track if entire pools are selected
|
||||||
|
let selectedPools: Set<string> = new Set();
|
||||||
|
|
||||||
|
function togglePoolExpansion(poolGuid: string) {
|
||||||
|
const newExpanded = new Set(expandedPools);
|
||||||
|
if (newExpanded.has(poolGuid)) {
|
||||||
|
newExpanded.delete(poolGuid);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(poolGuid);
|
||||||
|
}
|
||||||
|
expandedPools = newExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePoolSelection(pool: CustomTAMapPool) {
|
||||||
|
const newSelectedPools = new Set(selectedPools);
|
||||||
|
const newSelectedMaps = new Set(selectedMaps);
|
||||||
|
|
||||||
|
if (selectedPools.has(pool.guid)) {
|
||||||
|
// Unselect pool and all its maps
|
||||||
|
newSelectedPools.delete(pool.guid);
|
||||||
|
pool.maps.forEach(map => {
|
||||||
|
newSelectedMaps.delete(map.taData.guid);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Select pool and all its maps
|
||||||
|
newSelectedPools.add(pool.guid);
|
||||||
|
pool.maps.forEach(map => {
|
||||||
|
newSelectedMaps.add(map.taData.guid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPools = newSelectedPools;
|
||||||
|
selectedMaps = newSelectedMaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMapSelection(map: CustomMap, poolGuid: string) {
|
||||||
|
const newSelectedMaps = new Set(selectedMaps);
|
||||||
|
const newSelectedPools = new Set(selectedPools);
|
||||||
|
|
||||||
|
if (selectedMaps.has(map.taData.guid)) {
|
||||||
|
newSelectedMaps.delete(map.taData.guid);
|
||||||
|
// If pool was fully selected, unselect it
|
||||||
|
newSelectedPools.delete(poolGuid);
|
||||||
|
} else {
|
||||||
|
newSelectedMaps.add(map.taData.guid);
|
||||||
|
|
||||||
|
// Check if all maps in this pool are now selected
|
||||||
|
const pool = mapPools.find(p => p.guid === poolGuid);
|
||||||
|
if (pool && pool.maps.every(m => newSelectedMaps.has(m.taData.guid))) {
|
||||||
|
newSelectedPools.add(poolGuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedMaps = newSelectedMaps;
|
||||||
|
selectedPools = newSelectedPools;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDifficultyName(difficulty: number): string {
|
||||||
|
const difficultyMap: { [key: number]: string } = {
|
||||||
|
0: "Easy",
|
||||||
|
1: "Normal",
|
||||||
|
2: "Hard",
|
||||||
|
3: "Expert",
|
||||||
|
4: "Expert+"
|
||||||
|
};
|
||||||
|
return difficultyMap[difficulty] || "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModifiersText(modifiersBitmap: number): string {
|
||||||
|
// This would need your actual modifiers mapping
|
||||||
|
// For now, just show if modifiers are present
|
||||||
|
return modifiersBitmap > 0 ? "Modifiers Active" : "No Modifiers";
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDuration(map: CustomMap): string {
|
||||||
|
// You mentioned you have a function for this - placeholder implementation
|
||||||
|
if (map.beatsaverData?.metadata?.duration) {
|
||||||
|
const duration = map.beatsaverData.metadata.duration;
|
||||||
|
const minutes = Math.floor(duration / 60);
|
||||||
|
const seconds = duration % 60;
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
return "0:00";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCoverUrl(map: CustomMap): string {
|
||||||
|
return map.beatsaverData?.versions?.[0]?.coverURL || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddMaps() {
|
||||||
|
const mapsToAdd: CustomMap[] = [];
|
||||||
|
|
||||||
|
mapPools.forEach(pool => {
|
||||||
|
pool.maps.forEach(map => {
|
||||||
|
if (selectedMaps.has(map.taData.guid)) {
|
||||||
|
mapsToAdd.push(map);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mapsToAdd.length > 0) {
|
||||||
|
dispatch('addMaps', { maps: mapsToAdd });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedCount = selectedMaps.size;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="popup-overlay" on:click={handleClose}>
|
||||||
|
<div class="popup-container" on:click|stopPropagation>
|
||||||
|
<div class="popup-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h3>Add Maps from TA Pool</h3>
|
||||||
|
</div>
|
||||||
|
<button class="close-button" on:click={handleClose}>
|
||||||
|
<span class="material-icons">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="popup-content">
|
||||||
|
<div class="form-sections">
|
||||||
|
<div class="form-section">
|
||||||
|
<h4>Map Pools</h4>
|
||||||
|
<p class="section-desc">Select individual maps or entire pools to add to your match</p>
|
||||||
|
|
||||||
|
<div class="pools-list">
|
||||||
|
{#each mapPools as pool (pool.guid)}
|
||||||
|
<div class="pool-container">
|
||||||
|
<div class="pool-header">
|
||||||
|
<div class="pool-info">
|
||||||
|
<div class="pool-image">
|
||||||
|
<img src={bufferToImageUrl(pool.image)} alt={pool.name} />
|
||||||
|
</div>
|
||||||
|
<div class="pool-details">
|
||||||
|
<h5 class="pool-name">{pool.name}</h5>
|
||||||
|
<span class="pool-count">{pool.maps.length} maps</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pool-controls">
|
||||||
|
<label class="pool-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedPools.has(pool.guid)}
|
||||||
|
on:change={() => togglePoolSelection(pool)}
|
||||||
|
/>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="expand-button"
|
||||||
|
on:click={() => togglePoolExpansion(pool.guid)}
|
||||||
|
class:expanded={expandedPools.has(pool.guid)}
|
||||||
|
>
|
||||||
|
<span class="material-icons">expand_more</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if expandedPools.has(pool.guid)}
|
||||||
|
<div class="maps-list">
|
||||||
|
{#each pool.maps as map (map.taData.guid)}
|
||||||
|
<div class="map-item">
|
||||||
|
<div class="map-checkbox">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedMaps.has(map.taData.guid)}
|
||||||
|
on:change={() => toggleMapSelection(map, pool.guid)}
|
||||||
|
/>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="map-cover">
|
||||||
|
<img src={getCoverUrl(map)} alt={map.taData.gameplayParameters?.beatmap?.name} />
|
||||||
|
</div>
|
||||||
|
<div class="map-info">
|
||||||
|
<div class="map-title">{map.taData.gameplayParameters?.beatmap?.name}</div>
|
||||||
|
<div class="map-details">
|
||||||
|
<span class="map-duration">{calculateDuration(map)}</span>
|
||||||
|
<span class="map-separator">•</span>
|
||||||
|
<span class="map-difficulty {getDifficultyName(map.taData.gameplayParameters?.beatmap?.difficulty || 4).toLowerCase()}">
|
||||||
|
{getDifficultyName(map.taData.gameplayParameters?.beatmap?.difficulty || 4)}
|
||||||
|
</span>
|
||||||
|
<span class="map-separator">•</span>
|
||||||
|
<span class="map-modifiers">{getModifiersText(map.taData.gameplayParameters?.gameplayModifiers?.options || 0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="action-button cancel" on:click={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-button save"
|
||||||
|
on:click={handleAddMaps}
|
||||||
|
disabled={selectedCount === 0}
|
||||||
|
>
|
||||||
|
<span class="material-icons">add</span>
|
||||||
|
Add {selectedCount} Maps
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popup-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-container {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: 90vh;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h4 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-desc {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: -0.5rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pools-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-container {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--bg-tertiary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-image {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-count {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button:hover {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.maps-list {
|
||||||
|
border-top: 1px solid var(--bg-tertiary);
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-checkbox {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-cover {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-title {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-separator {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-difficulty {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-difficulty.easy {
|
||||||
|
background-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-difficulty.normal {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-difficulty.hard {
|
||||||
|
background-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-difficulty.expert {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-difficulty.expert\+ {
|
||||||
|
background-color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-duration,
|
||||||
|
.map-modifiers {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox styling */
|
||||||
|
.pool-checkbox,
|
||||||
|
.map-checkbox label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-checkbox input,
|
||||||
|
.map-checkbox input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
border: 2px solid var(--bg-tertiary);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
left: 0.3125rem;
|
||||||
|
top: 0.125rem;
|
||||||
|
width: 0.3125rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .checkmark {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.cancel {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.cancel:hover {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.save {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.save:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.popup-container {
|
||||||
|
width: 95%;
|
||||||
|
max-height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-header {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-info {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-image {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-item {
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-cover {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import type { BeatSaverMap } from "$lib/services/beatsaver";
|
import type { BeatSaverMap } from "$lib/services/beatsaver";
|
||||||
|
import { type Modifier, allGameModifiers } from "$lib/services/gamePlayModifiers";
|
||||||
|
|
||||||
export let tournamentGuid: string;
|
export let tournamentGuid: string;
|
||||||
export let mode: 'qualifier' | 'playing' = 'playing';
|
export let mode: 'qualifier' | 'playing' = 'playing';
|
||||||
|
|
@ -38,177 +39,18 @@
|
||||||
// Qualifier-specific settings
|
// Qualifier-specific settings
|
||||||
let limitAttemptsEnabled = false;
|
let limitAttemptsEnabled = false;
|
||||||
let numberOfAttempts = 1;
|
let numberOfAttempts = 1;
|
||||||
let disableScoresaberSubmission = false;
|
|
||||||
let showScoreboard = false;
|
let showScoreboard = false;
|
||||||
|
|
||||||
// Tournament Assistant settings for both modes
|
// Tournament Assistant settings for both modes
|
||||||
let disableFail = false;
|
let disableFail = false;
|
||||||
let disablePause = false;
|
let disablePause = false;
|
||||||
|
let disableScoresaberSubmission = false;
|
||||||
|
|
||||||
// Playing-specific settings
|
// Playing-specific settings
|
||||||
let disableCustomNotesOnStream = false;
|
let disableCustomNotesOnStream = false;
|
||||||
|
|
||||||
// Beat Saber Modifiers with incompatibility groups
|
|
||||||
interface Modifier {
|
|
||||||
id: GameplayModifiers_GameOptions;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
enabled: boolean;
|
|
||||||
group: string;
|
|
||||||
incompatibilityGroup?: string; // Modifiers in same group are mutually exclusive
|
|
||||||
allowedInMode?: ('qualifier' | 'playing')[];
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MapDifficulty {
|
|
||||||
"Easy" = 0,
|
|
||||||
"Normal" = 1,
|
|
||||||
"Hard" = 2,
|
|
||||||
"Expert" = 3,
|
|
||||||
"Expert+" = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game modifiers with proper grouping and mode restrictions
|
// Game modifiers with proper grouping and mode restrictions
|
||||||
let gameModifiers: Modifier[] = [
|
let gameModifiers: Modifier[] = allGameModifiers;
|
||||||
// Failure Prevention (mutually exclusive)
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.NoFail,
|
|
||||||
name: "No Fail",
|
|
||||||
description: "You can't fail the level",
|
|
||||||
enabled: false,
|
|
||||||
group: "Failure Prevention",
|
|
||||||
incompatibilityGroup: "failure",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.InstaFail,
|
|
||||||
name: "One Life",
|
|
||||||
description: "You only have one life",
|
|
||||||
enabled: false,
|
|
||||||
group: "Failure Prevention",
|
|
||||||
incompatibilityGroup: "failure",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.BatteryEnergy,
|
|
||||||
name: "Four Lives",
|
|
||||||
description: "You have four lives",
|
|
||||||
enabled: false,
|
|
||||||
group: "Failure Prevention",
|
|
||||||
incompatibilityGroup: "failure",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
|
|
||||||
// Environment Modifiers
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.NoBombs,
|
|
||||||
name: "No Bombs",
|
|
||||||
description: "No bombs will appear",
|
|
||||||
enabled: false,
|
|
||||||
group: "Environment",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.NoObstacles,
|
|
||||||
name: "No Walls",
|
|
||||||
description: "No walls will appear",
|
|
||||||
enabled: false,
|
|
||||||
group: "Environment",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
|
|
||||||
// Note Modifiers (some mutually exclusive)
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.NoArrows,
|
|
||||||
name: "No Arrows",
|
|
||||||
description: "All notes can be cut in any direction",
|
|
||||||
enabled: false,
|
|
||||||
group: "Note Modifiers",
|
|
||||||
incompatibilityGroup: "arrows",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.DisappearingArrows,
|
|
||||||
name: "Disappearing Arrows",
|
|
||||||
description: "Arrows disappear as they approach you",
|
|
||||||
enabled: false,
|
|
||||||
group: "Note Modifiers",
|
|
||||||
incompatibilityGroup: "arrows",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.GhostNotes,
|
|
||||||
name: "Ghost Notes",
|
|
||||||
description: "Note colors are hidden",
|
|
||||||
enabled: false,
|
|
||||||
group: "Note Modifiers",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
|
|
||||||
// Speed Modifiers (mutually exclusive)
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.SlowSong,
|
|
||||||
name: "Slower Song",
|
|
||||||
description: "Reduces the song speed by 15%",
|
|
||||||
enabled: false,
|
|
||||||
group: "Speed",
|
|
||||||
incompatibilityGroup: "speed",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.FastSong,
|
|
||||||
name: "Faster Song",
|
|
||||||
description: "Increases the song speed by 20%",
|
|
||||||
enabled: false,
|
|
||||||
group: "Speed",
|
|
||||||
incompatibilityGroup: "speed",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.SuperFastSong,
|
|
||||||
name: "Super Fast Song",
|
|
||||||
description: "Increases the song speed by 50%",
|
|
||||||
enabled: false,
|
|
||||||
group: "Speed",
|
|
||||||
incompatibilityGroup: "speed",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
|
|
||||||
// Visual Modifiers (some mutually exclusive)
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.SmallCubes,
|
|
||||||
name: "Small Notes",
|
|
||||||
description: "Notes are smaller",
|
|
||||||
enabled: false,
|
|
||||||
group: "Visual",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.ProMode,
|
|
||||||
name: "Pro Mode",
|
|
||||||
description: "Makes notes smaller, removes debris, and adds hit scores",
|
|
||||||
enabled: false,
|
|
||||||
group: "Visual",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
},
|
|
||||||
|
|
||||||
// Special Modifiers
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.ZenMode,
|
|
||||||
name: "Zen Mode",
|
|
||||||
description: "No fail, no bombs, reduced obstacles",
|
|
||||||
enabled: false,
|
|
||||||
group: "Special",
|
|
||||||
allowedInMode: ['playing'] // Only for playing mode
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: GameplayModifiers_GameOptions.StrictAngles,
|
|
||||||
name: "Strict Angles",
|
|
||||||
description: "Stricter angle enforcement for cuts",
|
|
||||||
enabled: false,
|
|
||||||
group: "Special",
|
|
||||||
allowedInMode: ['qualifier', 'playing']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Initialize from existing map data
|
// Initialize from existing map data
|
||||||
function initializeFromMap() {
|
function initializeFromMap() {
|
||||||
|
|
|
||||||
828
src/lib/components/popups/ResultsForMap.svelte
Normal file
828
src/lib/components/popups/ResultsForMap.svelte
Normal file
|
|
@ -0,0 +1,828 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { type Map, GameplayModifiers_GameOptions, Push_SongFinished } from 'moons-ta-client';
|
||||||
|
import type { BeatSaverMap } from '$lib/services/beatsaver.js';
|
||||||
|
import { type ScoreWithAccuracy, type PreviousResults, MapDifficulty } from '$lib/interfaces/match.interfaces';
|
||||||
|
|
||||||
|
export let mapData: PreviousResults | null;
|
||||||
|
export let isVisible: boolean = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
// Copy options
|
||||||
|
let includeMapInfo = true;
|
||||||
|
let includeEndTime = false;
|
||||||
|
let includeMisses = true;
|
||||||
|
let includeBadCuts = true;
|
||||||
|
let includeAccuracy = true;
|
||||||
|
|
||||||
|
// Modifiers mapping
|
||||||
|
const modifierNameMap: Record<number, string> = {
|
||||||
|
[GameplayModifiers_GameOptions.None]: "",
|
||||||
|
[GameplayModifiers_GameOptions.NoFail]: "No Fail",
|
||||||
|
[GameplayModifiers_GameOptions.NoBombs]: "No Bombs",
|
||||||
|
[GameplayModifiers_GameOptions.NoArrows]: "No Arrows",
|
||||||
|
[GameplayModifiers_GameOptions.NoObstacles]: "No Walls",
|
||||||
|
[GameplayModifiers_GameOptions.SlowSong]: "Slower Song",
|
||||||
|
[GameplayModifiers_GameOptions.InstaFail]: "One Life",
|
||||||
|
[GameplayModifiers_GameOptions.FailOnClash]: "Fail on Clash",
|
||||||
|
[GameplayModifiers_GameOptions.BatteryEnergy]: "Four Lives",
|
||||||
|
[GameplayModifiers_GameOptions.FastNotes]: "Fast Notes",
|
||||||
|
[GameplayModifiers_GameOptions.FastSong]: "Faster Song",
|
||||||
|
[GameplayModifiers_GameOptions.DisappearingArrows]: "Disappearing Arrows",
|
||||||
|
[GameplayModifiers_GameOptions.GhostNotes]: "Ghost Notes",
|
||||||
|
[GameplayModifiers_GameOptions.DemoNoFail]: "Demo No Fail",
|
||||||
|
[GameplayModifiers_GameOptions.DemoNoObstacles]: "Demo No Obstacles",
|
||||||
|
[GameplayModifiers_GameOptions.StrictAngles]: "Strict Angles",
|
||||||
|
[GameplayModifiers_GameOptions.ProMode]: "Pro Mode",
|
||||||
|
[GameplayModifiers_GameOptions.ZenMode]: "Zen Mode",
|
||||||
|
[GameplayModifiers_GameOptions.SmallCubes]: "Small Notes",
|
||||||
|
[GameplayModifiers_GameOptions.SuperFastSong]: "Super Fast Song"
|
||||||
|
};
|
||||||
|
|
||||||
|
function closePopup() {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveModifiers(gameplayModifiers: number): string[] {
|
||||||
|
const activeModifiers: string[] = [];
|
||||||
|
const values = Object.values(GameplayModifiers_GameOptions)
|
||||||
|
.filter((value): value is number =>
|
||||||
|
typeof value === 'number' &&
|
||||||
|
value !== GameplayModifiers_GameOptions.None &&
|
||||||
|
value > 0
|
||||||
|
)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
if ((gameplayModifiers & value) === value) {
|
||||||
|
const name = modifierNameMap[value];
|
||||||
|
if (name && name.trim() !== "") {
|
||||||
|
activeModifiers.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeModifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDifficultyName(difficulty: number): string {
|
||||||
|
return MapDifficulty[difficulty] || 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBPM(bpm: number): string {
|
||||||
|
return Math.round(bpm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCompletionStatus(score: ScoreWithAccuracy, songDuration: number): string {
|
||||||
|
const timeDifference = Math.abs(score.endTime - songDuration);
|
||||||
|
if (timeDifference <= 2) {
|
||||||
|
return 'Completed';
|
||||||
|
} else {
|
||||||
|
return `Exited early at: ${formatTime(score.endTime)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCompleted(score: ScoreWithAccuracy, songDuration: number): boolean {
|
||||||
|
const timeDifference = Math.abs(score.endTime - songDuration);
|
||||||
|
return timeDifference <= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyScoresToClipboard() {
|
||||||
|
const mapName = mapData!.beatsaverData.name || 'Unknown Song';
|
||||||
|
const artist = mapData!.beatsaverData.metadata?.songAuthorName || 'Unknown Artist';
|
||||||
|
const difficulty = getDifficultyName(mapData!.taData.gameplayParameters?.beatmap?.difficulty || 0);
|
||||||
|
const songDuration = mapData!.beatsaverData.metadata?.duration || 0;
|
||||||
|
|
||||||
|
let discordMessage = '';
|
||||||
|
|
||||||
|
if (includeMapInfo) {
|
||||||
|
discordMessage += `## **${mapName}** by ${artist}\n`;
|
||||||
|
discordMessage += `**Difficulty:** ${difficulty}\n`;
|
||||||
|
|
||||||
|
const modifiers = getActiveModifiers(mapData!.taData.gameplayParameters?.gameplayModifiers?.options || 0);
|
||||||
|
if (modifiers.length > 0) {
|
||||||
|
discordMessage += `**Modifiers:** ${modifiers.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
discordMessage += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort scores by total score (descending)
|
||||||
|
const sortedScores = [...mapData!.scores].sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
sortedScores.forEach((score, index) => {
|
||||||
|
const position = index + 1;
|
||||||
|
const positionText = position === 1 ? '1st' : position === 2 ? '2nd' : position === 3 ? '3rd' : `${position}th`;
|
||||||
|
const displayName = score.player?.name || score.player?.discordInfo?.username || 'Unknown Player';
|
||||||
|
|
||||||
|
discordMessage += `**${positionText} ${displayName}**\n`;
|
||||||
|
discordMessage += `Score: ${score.score.toLocaleString()}`;
|
||||||
|
|
||||||
|
if (includeAccuracy) {
|
||||||
|
discordMessage += ` | Accuracy: ${score.accuracy}%\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeEndTime) {
|
||||||
|
discordMessage += `-# `;
|
||||||
|
const completionStatus = getCompletionStatus(score, songDuration);
|
||||||
|
discordMessage += `Status: ${completionStatus}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMisses || includeBadCuts) {
|
||||||
|
let errorLine = '';
|
||||||
|
if (includeMisses) errorLine += `Misses: ${score.misses}`;
|
||||||
|
if (includeMisses && includeBadCuts) errorLine += ' | ';
|
||||||
|
if (includeBadCuts) errorLine += `Bad Cuts: ${score.badCuts}`;
|
||||||
|
discordMessage += `${errorLine}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
discordMessage += '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(discordMessage).then(() => {
|
||||||
|
console.log('Scores copied to clipboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: activeModifiers = getActiveModifiers(mapData?.taData.gameplayParameters?.gameplayModifiers?.options || 0);
|
||||||
|
$: sortedScores = mapData?.scores ? [...mapData.scores].sort((a, b) => b.score - a.score) : [];
|
||||||
|
$: songDuration = mapData?.beatsaverData.metadata?.duration || 0;
|
||||||
|
$: isAwaitingScores = mapData?.completionType === 'Still Awaiting Scores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isVisible && mapData}
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="popup-overlay" on:click={closePopup}>
|
||||||
|
<div class="popup-content" on:click|stopPropagation>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="popup-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="map-cover">
|
||||||
|
<img
|
||||||
|
src={mapData.beatsaverData.versions[0]?.coverURL || '/default-song-cover.png'}
|
||||||
|
alt="Map Cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="map-info">
|
||||||
|
<h2 class="map-title">{mapData.beatsaverData.name || 'Unknown Song'}</h2>
|
||||||
|
<p class="map-artist">{mapData.beatsaverData.metadata?.songAuthorName || 'Unknown Artist'}</p>
|
||||||
|
<p class="map-mapper">Mapped by {mapData.beatsaverData.metadata?.levelAuthorName || 'Unknown Mapper'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="close-button" on:click={closePopup}>
|
||||||
|
<span class="material-icons">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Awaiting Scores Indicator -->
|
||||||
|
{#if isAwaitingScores}
|
||||||
|
<div class="awaiting-scores">
|
||||||
|
<div class="loader"></div>
|
||||||
|
<span>Still awaiting scores...</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Map Details -->
|
||||||
|
<div class="map-details-section">
|
||||||
|
<h3>Map Details</h3>
|
||||||
|
<div class="details-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="material-icons">schedule</span>
|
||||||
|
<span class="detail-label">Duration</span>
|
||||||
|
<span class="detail-value">{formatTime(songDuration)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="material-icons">speed</span>
|
||||||
|
<span class="detail-label">BPM</span>
|
||||||
|
<span class="detail-value">{formatBPM(mapData.beatsaverData.metadata?.bpm || 0)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="material-icons">star</span>
|
||||||
|
<span class="detail-label">Difficulty</span>
|
||||||
|
<span class="detail-value difficulty-badge difficulty-{mapData.taData.gameplayParameters?.beatmap?.difficulty}">
|
||||||
|
{getDifficultyName(mapData.taData.gameplayParameters?.beatmap?.difficulty || 0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if activeModifiers.length > 0}
|
||||||
|
<div class="modifiers-section">
|
||||||
|
<h4>Active Modifiers</h4>
|
||||||
|
<div class="modifiers-list">
|
||||||
|
{#each activeModifiers as modifier}
|
||||||
|
<span class="modifier-tag">
|
||||||
|
<span class="material-icons">tune</span>
|
||||||
|
{modifier}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Copy Options -->
|
||||||
|
<div class="copy-options-section">
|
||||||
|
<h3>Copy Options</h3>
|
||||||
|
<div class="copy-controls">
|
||||||
|
<div class="toggle-row">
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" bind:checked={includeMapInfo} />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
Include Map Info
|
||||||
|
</label>
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" bind:checked={includeAccuracy} />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
Include Accuracy
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-row">
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" bind:checked={includeMisses} />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
Include Misses
|
||||||
|
</label>
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" bind:checked={includeBadCuts} />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
Include Bad Cuts
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-row">
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" bind:checked={includeEndTime} />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
Include End Time
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="copy-button" on:click={copyScoresToClipboard}>
|
||||||
|
<span class="material-icons">content_copy</span>
|
||||||
|
Copy Scores to Clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scores Section -->
|
||||||
|
<div class="scores-section">
|
||||||
|
<h3>Scores ({sortedScores.length})</h3>
|
||||||
|
{#if sortedScores.length === 0}
|
||||||
|
<div class="no-scores">
|
||||||
|
<span class="material-icons">leaderboard</span>
|
||||||
|
<p>No scores available</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="scores-list">
|
||||||
|
{#each sortedScores as score, index}
|
||||||
|
{@const position = index + 1}
|
||||||
|
{@const displayName = score.player?.name || score.player?.discordInfo?.username || 'Unknown Player'}
|
||||||
|
{@const completed = isCompleted(score, songDuration)}
|
||||||
|
<div class="score-card {completed ? 'completed' : 'exited-early'}">
|
||||||
|
<div class="score-position">
|
||||||
|
<span class="position-number">{position}</span>
|
||||||
|
<span class="position-suffix">{position === 1 ? 'st' : position === 2 ? 'nd' : position === 3 ? 'rd' : 'th'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="score-info">
|
||||||
|
<div class="player-name">{displayName}</div>
|
||||||
|
<div class="score-stats">
|
||||||
|
<div class="stat-item primary">
|
||||||
|
<span class="material-icons">emoji_events</span>
|
||||||
|
<span class="stat-value">{score.score.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="material-icons">percent</span>
|
||||||
|
<span class="stat-value">{score.accuracy}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item error">
|
||||||
|
<span class="material-icons">close</span>
|
||||||
|
<span class="stat-value">{score.misses}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item error">
|
||||||
|
<span class="material-icons">content_cut</span>
|
||||||
|
<span class="stat-value">{score.badCuts}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="completion-status">
|
||||||
|
<span class="material-icons">
|
||||||
|
{completed ? 'check_circle' : 'exit_to_app'}
|
||||||
|
</span>
|
||||||
|
<span class="status-text">
|
||||||
|
{getCompletionStatus(score, songDuration)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popup-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 1rem;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-cover {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-artist {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-mapper {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.awaiting-scores {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background-color: rgba(245, 158, 11, 0.1);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
|
border-left: 4px solid #F59E0B;
|
||||||
|
margin: 0 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #F59E0B;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: 2px solid rgba(245, 158, 11, 0.3);
|
||||||
|
border-top: 2px solid #F59E0B;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-details-section,
|
||||||
|
.copy-options-section,
|
||||||
|
.scores-section {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scores-section {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-details-section h3,
|
||||||
|
.copy-options-section h3,
|
||||||
|
.scores-section h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .material-icons {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-0 { background-color: #10B981; color: white; }
|
||||||
|
.difficulty-1 { background-color: #3B82F6; color: white; }
|
||||||
|
.difficulty-2 { background-color: #F59E0B; color: white; }
|
||||||
|
.difficulty-3 { background-color: #EF4444; color: white; }
|
||||||
|
.difficulty-4 { background-color: #8B5CF6; color: white; }
|
||||||
|
|
||||||
|
.modifiers-section h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifiers-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifier-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background-color: rgba(79, 70, 229, 0.1);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid rgba(79, 70, 229, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifier-tag .material-icons {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-controls {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: relative;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"]:checked + .toggle-slider {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"]:checked + .toggle-slider::before {
|
||||||
|
transform: translateX(1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 0 0 0 var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
box-shadow: 0 0 15px var(--accent-glow);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scores {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scores .material-icons {
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scores-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-card.completed {
|
||||||
|
border-left-color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-card.exited-early {
|
||||||
|
border-left-color: #EF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-card:hover {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-position {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-number {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-suffix {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item.primary {
|
||||||
|
background-color: rgba(251, 191, 36, 0.1);
|
||||||
|
color: #F59E0B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item.error {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #EF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item .material-icons {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-status .material-icons {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-card.completed .completion-status {
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-card.exited-early .completion-status {
|
||||||
|
color: #EF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-font-feature-settings: 'liga';
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling */
|
||||||
|
.popup-content::-webkit-scrollbar {
|
||||||
|
width: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--text-secondary);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.popup-overlay {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
max-height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-card {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"discordAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=http%3A%2F%2Flocalhost%3A1420%2FdiscordAuth&scope=identify",
|
"discordAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=https%3A%2F%2Fstaging.taui.shyyluna.dev%2FdiscordAuth&scope=identify",
|
||||||
"bkAPIUrl": "https://api.beatkhana.com/api",
|
"bkAPIUrl": "https://api.beatkhana.com/api",
|
||||||
"prodAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=https%3A%2F%2Ftaui.shyyluna.dev%2FdiscordAuth&scope=identify",
|
"prodAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=https%3A%2F%2Ftaui.shyyluna.dev%2FdiscordAuth&scope=identify",
|
||||||
"stagingAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=https%3A%2F%2Fstaging.taui.shyyluna.dev%2FdiscordAuth&scope=identify"
|
"stagingAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=https%3A%2F%2Fstaging.taui.shyyluna.dev%2FdiscordAuth&scope=identify",
|
||||||
|
"devAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=http%3A%2F%2Flocalhost%3A1420%2FdiscordAuth&scope=identify"
|
||||||
}
|
}
|
||||||
61
src/lib/interfaces/match.interfaces.ts
Normal file
61
src/lib/interfaces/match.interfaces.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// src/lib/types/match.ts
|
||||||
|
import type { Map, User, RealtimeScore, Push_SongFinished } from 'moons-ta-client';
|
||||||
|
import type { BeatSaverMap } from '$lib/services/beatsaver.js';
|
||||||
|
|
||||||
|
export interface CustomMap {
|
||||||
|
taData: Map;
|
||||||
|
beatsaverData: BeatSaverMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RealTimeScoreForPlayers {
|
||||||
|
player: User;
|
||||||
|
recentScore: RealtimeScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScoreWithAccuracy extends Push_SongFinished {
|
||||||
|
accuracy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreviousResults {
|
||||||
|
taData: Map;
|
||||||
|
beatsaverData: BeatSaverMap;
|
||||||
|
scores: ScoreWithAccuracy[];
|
||||||
|
completionType: 'Completed' | 'Still Awaiting Scores' | 'Exited To Menu';
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MapDifficulty {
|
||||||
|
"Easy" = 0,
|
||||||
|
"Normal" = 1,
|
||||||
|
"Hard" = 2,
|
||||||
|
"Expert" = 3,
|
||||||
|
"ExpertPlus" = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const colorPresets = {
|
||||||
|
primary: 'var(--accent-glow)',
|
||||||
|
secondary: 'var(--text-secondary)',
|
||||||
|
success: '#4CAF50',
|
||||||
|
warning: '#FFC107',
|
||||||
|
error: '#F44336',
|
||||||
|
info: '#2196F3',
|
||||||
|
default: 'var(--text-primary)'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ColorPreset = keyof typeof colorPresets;
|
||||||
|
export type ButtonType = 'primary' | 'secondary' | 'text' | 'outlined';
|
||||||
|
|
||||||
|
export interface ButtonConfig {
|
||||||
|
text: string;
|
||||||
|
type: ButtonType;
|
||||||
|
href?: string;
|
||||||
|
color?: ColorPreset;
|
||||||
|
icon?: string;
|
||||||
|
action?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomTAMapPool {
|
||||||
|
guid: string;
|
||||||
|
name: string;
|
||||||
|
image: Uint8Array;
|
||||||
|
maps: CustomMap[];
|
||||||
|
}
|
||||||
155
src/lib/services/gamePlayModifiers.ts
Normal file
155
src/lib/services/gamePlayModifiers.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { Response_ResponseType, Map, Characteristic, GameplayModifiers_GameOptions } from 'moons-ta-client';
|
||||||
|
|
||||||
|
// Beat Saber Modifiers with incompatibility groups
|
||||||
|
export interface Modifier {
|
||||||
|
id: GameplayModifiers_GameOptions;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
enabled: boolean;
|
||||||
|
group: string;
|
||||||
|
incompatibilityGroup?: string; // Modifiers in same group are mutually exclusive
|
||||||
|
allowedInMode?: ('qualifier' | 'playing')[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game modifiers with proper grouping and mode restrictions
|
||||||
|
export const allGameModifiers: Modifier[] = [
|
||||||
|
// Failure Prevention (mutually exclusive)
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.NoFail,
|
||||||
|
name: "No Fail",
|
||||||
|
description: "You can't fail the level",
|
||||||
|
enabled: false,
|
||||||
|
group: "Failure Prevention",
|
||||||
|
incompatibilityGroup: "failure",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.InstaFail,
|
||||||
|
name: "One Life",
|
||||||
|
description: "You only have one life",
|
||||||
|
enabled: false,
|
||||||
|
group: "Failure Prevention",
|
||||||
|
incompatibilityGroup: "failure",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.BatteryEnergy,
|
||||||
|
name: "Four Lives",
|
||||||
|
description: "You have four lives",
|
||||||
|
enabled: false,
|
||||||
|
group: "Failure Prevention",
|
||||||
|
incompatibilityGroup: "failure",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Environment Modifiers
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.NoBombs,
|
||||||
|
name: "No Bombs",
|
||||||
|
description: "No bombs will appear",
|
||||||
|
enabled: false,
|
||||||
|
group: "Environment",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.NoObstacles,
|
||||||
|
name: "No Walls",
|
||||||
|
description: "No walls will appear",
|
||||||
|
enabled: false,
|
||||||
|
group: "Environment",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Note Modifiers (some mutually exclusive)
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.NoArrows,
|
||||||
|
name: "No Arrows",
|
||||||
|
description: "All notes can be cut in any direction",
|
||||||
|
enabled: false,
|
||||||
|
group: "Note Modifiers",
|
||||||
|
incompatibilityGroup: "arrows",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.DisappearingArrows,
|
||||||
|
name: "Disappearing Arrows",
|
||||||
|
description: "Arrows disappear as they approach you",
|
||||||
|
enabled: false,
|
||||||
|
group: "Note Modifiers",
|
||||||
|
incompatibilityGroup: "arrows",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.GhostNotes,
|
||||||
|
name: "Ghost Notes",
|
||||||
|
description: "Note colors are hidden",
|
||||||
|
enabled: false,
|
||||||
|
group: "Note Modifiers",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Speed Modifiers (mutually exclusive)
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.SlowSong,
|
||||||
|
name: "Slower Song",
|
||||||
|
description: "Reduces the song speed by 15%",
|
||||||
|
enabled: false,
|
||||||
|
group: "Speed",
|
||||||
|
incompatibilityGroup: "speed",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.FastSong,
|
||||||
|
name: "Faster Song",
|
||||||
|
description: "Increases the song speed by 20%",
|
||||||
|
enabled: false,
|
||||||
|
group: "Speed",
|
||||||
|
incompatibilityGroup: "speed",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.SuperFastSong,
|
||||||
|
name: "Super Fast Song",
|
||||||
|
description: "Increases the song speed by 50%",
|
||||||
|
enabled: false,
|
||||||
|
group: "Speed",
|
||||||
|
incompatibilityGroup: "speed",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Visual Modifiers (some mutually exclusive)
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.SmallCubes,
|
||||||
|
name: "Small Notes",
|
||||||
|
description: "Notes are smaller",
|
||||||
|
enabled: false,
|
||||||
|
group: "Visual",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.ProMode,
|
||||||
|
name: "Pro Mode",
|
||||||
|
description: "Makes notes smaller, removes debris, and adds hit scores",
|
||||||
|
enabled: false,
|
||||||
|
group: "Visual",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Special Modifiers
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.ZenMode,
|
||||||
|
name: "Zen Mode",
|
||||||
|
description: "No fail, no bombs, reduced obstacles",
|
||||||
|
enabled: false,
|
||||||
|
group: "Special",
|
||||||
|
allowedInMode: ['playing'] // Only for playing mode
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: GameplayModifiers_GameOptions.StrictAngles,
|
||||||
|
name: "Strict Angles",
|
||||||
|
description: "Stricter angle enforcement for cuts",
|
||||||
|
enabled: false,
|
||||||
|
group: "Special",
|
||||||
|
allowedInMode: ['qualifier', 'playing']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
import Popup from "$lib/components/notifications/Popup.svelte";
|
import Popup from "$lib/components/notifications/Popup.svelte";
|
||||||
import InfoPopup from "$lib/components/notifications/InfoPopup.svelte";
|
import InfoPopup from "$lib/components/notifications/InfoPopup.svelte";
|
||||||
import EditMapModal from "$lib/components/popups/EditMap.svelte";
|
import EditMapModal from "$lib/components/popups/EditMap.svelte";
|
||||||
|
import AddMapsFromFile from "$lib/components/popups/AddMapsFromFile.svelte";
|
||||||
|
import { type BeatSaverMap } from "$lib/services/beatsaver.js";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import type { CustomMap } from "$lib/interfaces/match.interfaces.js";
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
|
|
@ -19,37 +22,7 @@
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
let poolName: string = "";
|
let poolName: string = "";
|
||||||
let maps: {
|
let maps: CustomMap[] = [];
|
||||||
taData: Map,
|
|
||||||
beatsaverData: BeatSaverMap
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum MapDifficulty {
|
enum MapDifficulty {
|
||||||
"Easy" = 0,
|
"Easy" = 0,
|
||||||
|
|
@ -59,17 +32,6 @@
|
||||||
"Expert+" = 4
|
"Expert+" = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Modifier {
|
|
||||||
id: GameplayModifiers_GameOptions;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
enabled: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
group?: string;
|
|
||||||
incompatibilityGroup?: string;
|
|
||||||
allowedInMode: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifierNameMap: Record<number, string> = {
|
const modifierNameMap: Record<number, string> = {
|
||||||
[GameplayModifiers_GameOptions.None]: "",
|
[GameplayModifiers_GameOptions.None]: "",
|
||||||
[GameplayModifiers_GameOptions.NoFail]: "No Fail",
|
[GameplayModifiers_GameOptions.NoFail]: "No Fail",
|
||||||
|
|
@ -109,6 +71,9 @@
|
||||||
let showSuccessNotification = false;
|
let showSuccessNotification = false;
|
||||||
let successMessage = "";
|
let successMessage = "";
|
||||||
|
|
||||||
|
// Import maps popup
|
||||||
|
let showLoadMapsFromFilePopup: boolean = false;
|
||||||
|
|
||||||
// Remove 'Bearer ' from the token
|
// Remove 'Bearer ' from the token
|
||||||
if ($authTokenStore) {
|
if ($authTokenStore) {
|
||||||
const cleanToken = $authTokenStore.replace('Bearer ', '');
|
const cleanToken = $authTokenStore.replace('Bearer ', '');
|
||||||
|
|
@ -134,6 +99,14 @@
|
||||||
currentEditMap = null;
|
currentEditMap = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openLoadMapsFromFilePopup() {
|
||||||
|
showLoadMapsFromFilePopup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLoadMapsFromFilePopup() {
|
||||||
|
showLoadMapsFromFilePopup = false;
|
||||||
|
}
|
||||||
|
|
||||||
function openDeleteConfirmation(mapId: string) {
|
function openDeleteConfirmation(mapId: string) {
|
||||||
mapToDelete = mapId;
|
mapToDelete = mapId;
|
||||||
showDeleteConfirmation = true;
|
showDeleteConfirmation = true;
|
||||||
|
|
@ -148,6 +121,21 @@
|
||||||
showSuccessNotification = false;
|
showSuccessNotification = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAddMapsFromFile(event: CustomEvent) {
|
||||||
|
const toAddMaps = (event.detail.maps as CustomMap[]);
|
||||||
|
|
||||||
|
const taDatasToAdd = await toAddMaps.map((map) => map.taData);
|
||||||
|
const addResponse = await client.addTournamentPoolMaps(tournamentGuid, poolGuid, taDatasToAdd);
|
||||||
|
|
||||||
|
console.log('Added multiple maps response:', addResponse);
|
||||||
|
fetchMapPoolData();
|
||||||
|
showSuccessNotification = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
showSuccessNotification = false;
|
||||||
|
}, 2500);
|
||||||
|
successMessage = "Map successfully updated!";
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMapUpdated(event: CustomEvent) {
|
async function handleMapUpdated(event: CustomEvent) {
|
||||||
await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
|
await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
|
||||||
fetchMapPoolData();
|
fetchMapPoolData();
|
||||||
|
|
@ -331,6 +319,10 @@
|
||||||
<span class="material-icons">add</span>
|
<span class="material-icons">add</span>
|
||||||
Add Map
|
Add Map
|
||||||
</button>
|
</button>
|
||||||
|
<button class="action-button add" on:click={() => openLoadMapsFromFilePopup()}>
|
||||||
|
<span class="material-icons">add</span>
|
||||||
|
Load Maps From File
|
||||||
|
</button>
|
||||||
<button class="action-button refresh" on:click={fetchMapPoolData}>
|
<button class="action-button refresh" on:click={fetchMapPoolData}>
|
||||||
<span class="material-icons">refresh</span>
|
<span class="material-icons">refresh</span>
|
||||||
Refresh
|
Refresh
|
||||||
|
|
@ -446,8 +438,7 @@
|
||||||
{#if showEditMapModal}
|
{#if showEditMapModal}
|
||||||
<EditMapModal
|
<EditMapModal
|
||||||
tournamentGuid={tournamentGuid}
|
tournamentGuid={tournamentGuid}
|
||||||
poolGuid={poolGuid}
|
map={maps.filter(map => map.taData.guid == currentEditMap?.guid)[0] || null}
|
||||||
map={maps.find(map => map.taData.guid == currentEditMap?.guid) || null}
|
|
||||||
on:close={closeEditMapModal}
|
on:close={closeEditMapModal}
|
||||||
on:mapUpdated={handleMapUpdated}
|
on:mapUpdated={handleMapUpdated}
|
||||||
on:mapAdded={handleMapAdded}
|
on:mapAdded={handleMapAdded}
|
||||||
|
|
@ -455,6 +446,13 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Add Maps From File Modal Component -->
|
||||||
|
<AddMapsFromFile
|
||||||
|
isOpen={showLoadMapsFromFilePopup}
|
||||||
|
on:close={closeLoadMapsFromFilePopup}
|
||||||
|
on:addMaps={handleAddMapsFromFile}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Info Popup -->
|
<!-- Info Popup -->
|
||||||
{#if showInfoPopup}
|
{#if showInfoPopup}
|
||||||
<InfoPopup content={infoPopupContent} onClose={closeInfoPopup} />
|
<InfoPopup content={infoPopupContent} onClose={closeInfoPopup} />
|
||||||
|
|
|
||||||
|
|
@ -4,43 +4,51 @@
|
||||||
import { bkAPIUrl } from '$lib/config.json';
|
import { bkAPIUrl } from '$lib/config.json';
|
||||||
import Notification from '$lib/components/notifications/Popup.svelte';
|
import Notification from '$lib/components/notifications/Popup.svelte';
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import { Match, Tournament, TAClient, Response_ResponseType, User, RealtimeScore, Push_SongFinished, Map, User_DownloadStates, User_PlayStates } from 'moons-ta-client';
|
import {
|
||||||
|
Match,
|
||||||
|
Tournament,
|
||||||
|
TAClient,
|
||||||
|
Response_ResponseType,
|
||||||
|
User,
|
||||||
|
RealtimeScore,
|
||||||
|
Push_SongFinished,
|
||||||
|
Map,
|
||||||
|
User_DownloadStates,
|
||||||
|
User_PlayStates,
|
||||||
|
Tournament_TournamentSettings_Pool
|
||||||
|
} from 'moons-ta-client';
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { fetchMapByLevelId, type BeatSaverMap } from "$lib/services/beatsaver.js";
|
import { fetchMapByLevelId, fetchMapsByLevelIds, type BeatSaverMap } from "$lib/services/beatsaver.js";
|
||||||
import SongQueue from "$lib/components/modules/SongQueue.svelte";
|
import SongQueue from "$lib/components/modules/SongQueue.svelte";
|
||||||
import EditMap from "$lib/components/popups/EditMap.svelte";
|
import EditMap from "$lib/components/popups/EditMap.svelte";
|
||||||
import Popup from "$lib/components/notifications/Popup.svelte";
|
import Popup from "$lib/components/notifications/Popup.svelte";
|
||||||
import PreviouslyPlayedSongs from "$lib/components/modules/PreviouslyPlayedSongs.svelte";
|
import PreviouslyPlayedSongs from "$lib/components/modules/PreviouslyPlayedSongs.svelte";
|
||||||
|
import ResultsForMap from "$lib/components/popups/ResultsForMap.svelte";
|
||||||
|
import AddMapsFromTAPool from "$lib/components/popups/AddMapsFromTAPool.svelte";
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import {
|
||||||
|
type CustomMap,
|
||||||
|
type RealTimeScoreForPlayers,
|
||||||
|
type ScoreWithAccuracy,
|
||||||
|
type PreviousResults,
|
||||||
|
type ButtonConfig,
|
||||||
|
type ColorPreset,
|
||||||
|
MapDifficulty,
|
||||||
|
|
||||||
|
type CustomTAMapPool
|
||||||
|
|
||||||
|
} from "$lib/interfaces/match.interfaces";
|
||||||
|
import AddMapsFromFile from "$lib/components/popups/AddMapsFromFile.svelte";
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
interface CustomMap {
|
// Route params
|
||||||
taData: Map,
|
|
||||||
beatsaverData: BeatSaverMap
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RealTimeScoreForPlayers {
|
|
||||||
player: User,
|
|
||||||
recentScore: RealtimeScore
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScoreWithAccuracy extends Push_SongFinished {
|
|
||||||
accuracy: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PreviousResults {
|
|
||||||
taData: Map;
|
|
||||||
beatsaverData: BeatSaverMap;
|
|
||||||
scores: ScoreWithAccuracy[];
|
|
||||||
completionType: 'Completed' | 'Still Awaiting Scores' | 'Exited To Menu';
|
|
||||||
}
|
|
||||||
|
|
||||||
let upcomingQueueMaps: CustomMap[] = [];
|
|
||||||
|
|
||||||
const tournamentGuid: string = data.tournamentGuid;
|
const tournamentGuid: string = data.tournamentGuid;
|
||||||
const matchGuid: string = data.matchGuid;
|
const matchGuid: string = data.matchGuid;
|
||||||
|
|
||||||
|
// Core state
|
||||||
let tournament: Tournament | undefined;
|
let tournament: Tournament | undefined;
|
||||||
let match: Match | undefined;
|
let match: Match | undefined;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
@ -51,29 +59,11 @@
|
||||||
let isMatchActive = false;
|
let isMatchActive = false;
|
||||||
let streamSyncEnabled = false;
|
let streamSyncEnabled = false;
|
||||||
|
|
||||||
|
// Song queue and results
|
||||||
|
let upcomingQueueMaps: CustomMap[] = [];
|
||||||
let realTimeScores: RealTimeScoreForPlayers[] = [];
|
let realTimeScores: RealTimeScoreForPlayers[] = [];
|
||||||
let previousMatchResults: PreviousResults[] = [];
|
let previousMatchResults: PreviousResults[] = [];
|
||||||
|
let showingResultsData: PreviousResults;
|
||||||
const colorPresets = {
|
|
||||||
primary: 'var(--accent-glow)',
|
|
||||||
secondary: 'var(--text-secondary)',
|
|
||||||
success: '#4CAF50',
|
|
||||||
warning: '#FFC107',
|
|
||||||
error: '#F44336',
|
|
||||||
info: '#2196F3',
|
|
||||||
default: 'var(--text-primary)'
|
|
||||||
};
|
|
||||||
type ColorPreset = keyof typeof colorPresets;
|
|
||||||
|
|
||||||
type ButtonType = 'primary' | 'secondary' | 'text' | 'outlined';
|
|
||||||
interface ButtonConfig {
|
|
||||||
text: string;
|
|
||||||
type: ButtonType;
|
|
||||||
href?: string;
|
|
||||||
color?: ColorPreset;
|
|
||||||
icon?: string;
|
|
||||||
action?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
let showKickConfirmModal = false;
|
let showKickConfirmModal = false;
|
||||||
|
|
@ -86,30 +76,60 @@
|
||||||
let modalIconColor: ColorPreset = 'error';
|
let modalIconColor: ColorPreset = 'error';
|
||||||
let modalButtons: ButtonConfig[] = [];
|
let modalButtons: ButtonConfig[] = [];
|
||||||
let modalCustomImage: string = "";
|
let modalCustomImage: string = "";
|
||||||
let modalAutoClose: number = 0; // no auto close
|
let modalAutoClose: number = 0;
|
||||||
|
|
||||||
|
// Map editing states
|
||||||
let showMapModal: boolean = false;
|
let showMapModal: boolean = false;
|
||||||
let currentlyEditingMap: CustomMap | null = null;
|
let currentlyEditingMap: CustomMap | null = null;
|
||||||
|
|
||||||
|
// Song loading states
|
||||||
let loadingSongForPlayers: boolean = false;
|
let loadingSongForPlayers: boolean = false;
|
||||||
let failedToLoadSongForPlayers: boolean = false;
|
let failedToLoadSongForPlayers: boolean = false;
|
||||||
|
|
||||||
|
// Results popup states
|
||||||
|
let showResultsPopup: boolean = false;
|
||||||
|
let dontShowResultsPopupForThisSong: boolean = false;
|
||||||
|
let currentlyShowingOldMapResults: boolean = false;
|
||||||
|
|
||||||
|
// Add maps from TA pool modal state
|
||||||
|
let taPools: Tournament_TournamentSettings_Pool[] = [];
|
||||||
|
let customTAPools: CustomTAMapPool[] = [];
|
||||||
|
let showAddFromTAPoolPopup: boolean = false;
|
||||||
|
let allowAddFromTAPool: boolean = true;
|
||||||
|
|
||||||
|
// Active song tracking
|
||||||
const activeSongPlayers = new Set<`${string}-${string}`>(); // Format: "userGuid-levelId"
|
const activeSongPlayers = new Set<`${string}-${string}`>(); // Format: "userGuid-levelId"
|
||||||
|
|
||||||
// Remove 'Bearer ' from the token
|
// Reactive statements
|
||||||
|
$: isAnyPlayerPlaying = matchPlayers.some(player => (player.playState as any) === User_PlayStates.InGame);
|
||||||
|
|
||||||
if ($authTokenStore) {
|
if ($authTokenStore) {
|
||||||
const cleanToken = $authTokenStore.replace('Bearer ', '');
|
const cleanToken = $authTokenStore.replace('Bearer ', '');
|
||||||
client.setAuthToken(cleanToken);
|
client.setAuthToken(cleanToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: isAnyPlayerPlaying = matchPlayers.some(player => (player.playState as any) === User_PlayStates.InGame);
|
// Show notification function
|
||||||
|
|
||||||
// Show notification function (you mentioned you have this)
|
|
||||||
function showNotification(message: string, type: 'warning' | 'error' | 'success' = 'warning') {
|
function showNotification(message: string, type: 'warning' | 'error' | 'success' = 'warning') {
|
||||||
// Your existing notification function
|
// Your existing notification function
|
||||||
console.log(`${type}: ${message}`);
|
console.log(`${type}: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleExitResultsPopupClicked() {
|
||||||
|
showResultsPopup = false;
|
||||||
|
if(!currentlyShowingOldMapResults) {
|
||||||
|
dontShowResultsPopupForThisSong = true;
|
||||||
|
}
|
||||||
|
currentlyShowingOldMapResults = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResultsPopupWithOldScores(event: CustomEvent) {
|
||||||
|
const oldData: PreviousResults = event.detail.map;
|
||||||
|
currentlyShowingOldMapResults = true;
|
||||||
|
showingResultsData = oldData;
|
||||||
|
showResultsPopup = true;
|
||||||
|
dontShowResultsPopupForThisSong = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle back to matches with confirmation if needed
|
// Handle back to matches with confirmation if needed
|
||||||
async function handleBackToMatches(endMatch: boolean) {
|
async function handleBackToMatches(endMatch: boolean) {
|
||||||
if(match!.leader !== client.stateManager.getSelfGuid()) {
|
if(match!.leader !== client.stateManager.getSelfGuid()) {
|
||||||
|
|
@ -344,11 +364,74 @@
|
||||||
|
|
||||||
if(tempMapData) {
|
if(tempMapData) {
|
||||||
currentSongData = tempMapData;
|
currentSongData = tempMapData;
|
||||||
|
if(!upcomingQueueMaps.some(x => x.taData.gameplayParameters?.beatmap?.levelId.toUpperCase() == currentSong.beatmap.levelId.toUpperCase())) {
|
||||||
|
upcomingQueueMaps = [
|
||||||
|
{
|
||||||
|
taData: match.selectedMap!,
|
||||||
|
beatsaverData: tempMapData!
|
||||||
|
},
|
||||||
|
...upcomingQueueMaps
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Current song beatsaver data fetch failed');
|
console.log('Current song beatsaver data fetch failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tournament = client.stateManager.getTournament(tournamentGuid);
|
||||||
|
|
||||||
|
if(!tournament!.settings!.enablePools) {
|
||||||
|
allowAddFromTAPool = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
taPools = tournament!.settings!.pools;
|
||||||
|
|
||||||
|
// First, collect all unique level IDs from all pools
|
||||||
|
const allLevelIds = taPools.flatMap(pool =>
|
||||||
|
pool.maps.map(map => map.gameplayParameters!.beatmap!.levelId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove duplicates to avoid fetching the same map multiple times
|
||||||
|
const uniqueLevelIds = [...new Set(allLevelIds)];
|
||||||
|
|
||||||
|
// Fetch all maps data in one go (your function handles chunking internally)
|
||||||
|
const allMapBeatSaverDatas = await fetchMapsByLevelIds(uniqueLevelIds);
|
||||||
|
|
||||||
|
// Create a lookup object for quick access
|
||||||
|
const mapDataLookup: { [levelId: string]: any } = {};
|
||||||
|
allMapBeatSaverDatas.forEach(mapData => {
|
||||||
|
if (mapData && mapData.versions[0].hash) {
|
||||||
|
mapDataLookup[mapData.versions[0].hash] = mapData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now process each pool using the pre-fetched data
|
||||||
|
customTAPools = await Promise.all(
|
||||||
|
taPools.map(async (pool) => {
|
||||||
|
const poolCustomMaps: CustomMap[] = pool.maps
|
||||||
|
.map(map => {
|
||||||
|
const levelId = map.gameplayParameters!.beatmap!.levelId;
|
||||||
|
const beatsaverData = mapDataLookup[(levelId.replace('custom_level_', ''))];
|
||||||
|
|
||||||
|
if (!beatsaverData) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
taData: map,
|
||||||
|
beatsaverData: beatsaverData
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(result => result !== null) as CustomMap[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: pool.name,
|
||||||
|
guid: pool.guid,
|
||||||
|
image: pool.image,
|
||||||
|
maps: poolCustomMaps
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMatchData() {
|
async function fetchMatchData() {
|
||||||
|
|
@ -399,6 +482,9 @@
|
||||||
async function startSongForMatch() {
|
async function startSongForMatch() {
|
||||||
if (!currentSong) return;
|
if (!currentSong) return;
|
||||||
|
|
||||||
|
showResultsPopup = false;
|
||||||
|
dontShowResultsPopupForThisSong = false;
|
||||||
|
|
||||||
const levelId = currentSong.beatmap.levelId;
|
const levelId = currentSong.beatmap.levelId;
|
||||||
const playingPlayerIds = matchPlayers.map(player => player.guid);
|
const playingPlayerIds = matchPlayers.map(player => player.guid);
|
||||||
|
|
||||||
|
|
@ -454,6 +540,8 @@
|
||||||
|
|
||||||
const player = matchPlayers.find(x => x.guid === rts.userGuid)!;
|
const player = matchPlayers.find(x => x.guid === rts.userGuid)!;
|
||||||
|
|
||||||
|
if(!player) return;
|
||||||
|
|
||||||
const index = realTimeScores.findIndex(x => x.player.guid === rts.userGuid);
|
const index = realTimeScores.findIndex(x => x.player.guid === rts.userGuid);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
|
@ -495,6 +583,39 @@
|
||||||
return playerScore;
|
return playerScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMaxScore(songInfo: BeatSaverMap, characteristic: string = "standard", difficulty: string): number {
|
||||||
|
const diff = songInfo.versions[0].diffs.find(
|
||||||
|
(x) => {
|
||||||
|
const charMatch = x.characteristic.toLowerCase() === characteristic.toLowerCase();
|
||||||
|
|
||||||
|
// Normalize difficulty strings for comparison
|
||||||
|
const searchDiff = difficulty.toLowerCase() === "expert+" ? "expertplus" : difficulty.toLowerCase();
|
||||||
|
const diffDiff = x.difficulty.toLowerCase() === "expert+" ? "expertplus" : x.difficulty.toLowerCase();
|
||||||
|
|
||||||
|
return charMatch && diffDiff === searchDiff;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return diff?.maxScore ?? 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to convert difficulty number to string
|
||||||
|
function getDifficultyAsString(difficulty: number): string {
|
||||||
|
return MapDifficulty[difficulty] || "ExpertPlus";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main accuracy calculation logic
|
||||||
|
function calculateAccuracy(score: Push_SongFinished, mapWithSongInfo: CustomMap): string {
|
||||||
|
const maxScore = getMaxScore(
|
||||||
|
mapWithSongInfo.beatsaverData,
|
||||||
|
score.beatmap?.characteristic?.serializedName ?? "Standard",
|
||||||
|
getDifficultyAsString(score.beatmap?.difficulty ?? 4) || "ExpertPlus",
|
||||||
|
);
|
||||||
|
|
||||||
|
const accuracy = ((score.score / maxScore) * 100).toFixed(2);
|
||||||
|
return accuracy;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSongFinished(rts: Push_SongFinished) {
|
async function handleSongFinished(rts: Push_SongFinished) {
|
||||||
if (rts.matchId !== matchGuid) return;
|
if (rts.matchId !== matchGuid) return;
|
||||||
|
|
||||||
|
|
@ -513,7 +634,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRts: ScoreWithAccuracy = {...rts, accuracy: 0};
|
let newRts: ScoreWithAccuracy = {...rts, accuracy: "0"};
|
||||||
|
newRts.accuracy = calculateAccuracy(rts, map);
|
||||||
|
|
||||||
// Check if any players are still playing this song
|
// Check if any players are still playing this song
|
||||||
const playersStillPlaying = Array.from(activeSongPlayers)
|
const playersStillPlaying = Array.from(activeSongPlayers)
|
||||||
|
|
@ -551,6 +673,9 @@
|
||||||
...previousMatchResults.filter((_, i) => i !== existingRecordIndex)
|
...previousMatchResults.filter((_, i) => i !== existingRecordIndex)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showingResultsData = previousMatchResults[0];
|
||||||
|
showResultsPopup = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMapUpdated(event: CustomEvent) {
|
async function handleMapUpdated(event: CustomEvent) {
|
||||||
|
|
@ -582,6 +707,23 @@
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleShowAddMapsFromTAPool() {
|
||||||
|
showAddFromTAPoolPopup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloseAddMapsFromTAPool() {
|
||||||
|
showAddFromTAPoolPopup = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddMapsFromTAPool(event: CustomEvent) {
|
||||||
|
showAddFromTAPoolPopup = false;
|
||||||
|
const mapsToAdd: CustomMap[] = event.detail.maps;
|
||||||
|
upcomingQueueMaps = [
|
||||||
|
...mapsToAdd,
|
||||||
|
...upcomingQueueMaps
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function closeEditMapModal() {
|
function closeEditMapModal() {
|
||||||
showMapModal = false;
|
showMapModal = false;
|
||||||
}
|
}
|
||||||
|
|
@ -606,6 +748,7 @@
|
||||||
const map: CustomMap = event.detail.map;
|
const map: CustomMap = event.detail.map;
|
||||||
loadingSongForPlayers = false;
|
loadingSongForPlayers = false;
|
||||||
failedToLoadSongForPlayers = false;
|
failedToLoadSongForPlayers = false;
|
||||||
|
dontShowResultsPopupForThisSong = false;
|
||||||
|
|
||||||
let playingPlayerIds = matchPlayers
|
let playingPlayerIds = matchPlayers
|
||||||
.filter(player => player.playState !== User_PlayStates.InGame)
|
.filter(player => player.playState !== User_PlayStates.InGame)
|
||||||
|
|
@ -704,7 +847,7 @@
|
||||||
End Match
|
End Match
|
||||||
</button>
|
</button>
|
||||||
<div class="match-info">
|
<div class="match-info">
|
||||||
<h2>Match: {'Unknown'}</h2>
|
<h2>Match: {match?.guid || 'Undefined'}</h2>
|
||||||
<p class="tournament-name">{tournament?.settings?.tournamentName}</p>
|
<p class="tournament-name">{tournament?.settings?.tournamentName}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -865,6 +1008,7 @@
|
||||||
on:editMap={handleEditMap}
|
on:editMap={handleEditMap}
|
||||||
on:removeMap={handleRemoveMapFromQueue}
|
on:removeMap={handleRemoveMapFromQueue}
|
||||||
on:songLoad={handleLoadMap}
|
on:songLoad={handleLoadMap}
|
||||||
|
on:addMapsFromTAPool={handleShowAddMapsFromTAPool}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -872,6 +1016,7 @@
|
||||||
<div class="queue-section">
|
<div class="queue-section">
|
||||||
<PreviouslyPlayedSongs
|
<PreviouslyPlayedSongs
|
||||||
maps={previousMatchResults}
|
maps={previousMatchResults}
|
||||||
|
on:showMapDetails={showResultsPopupWithOldScores}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -901,6 +1046,21 @@
|
||||||
autoClose={modalAutoClose}
|
autoClose={modalAutoClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ResultsForMap
|
||||||
|
mapData={showingResultsData}
|
||||||
|
isVisible={showResultsPopup && !dontShowResultsPopupForThisSong}
|
||||||
|
on:close={handleExitResultsPopupClicked}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if allowAddFromTAPool}
|
||||||
|
<AddMapsFromTAPool
|
||||||
|
isOpen={showAddFromTAPoolPopup}
|
||||||
|
mapPools={customTAPools}
|
||||||
|
on:close={handleCloseAddMapsFromTAPool}
|
||||||
|
on:addMaps={handleAddMapsFromTAPool}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.match-dashboard {
|
.match-dashboard {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1048,6 +1208,8 @@
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: 20rem;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section Styles */
|
/* Section Styles */
|
||||||
|
|
@ -1111,8 +1273,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
max-height: 50vh;
|
max-height: 20rem;
|
||||||
overflow-y: auto;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-card {
|
.player-card {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue