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 { type Map, GameplayModifiers_GameOptions, Push_SongFinished, RealtimeScore } from 'moons-ta-client';
|
||||
import type { BeatSaverMap } from '$lib/services/beatsaver.js';
|
||||
|
||||
interface ScoreWithAccuracy extends Push_SongFinished {
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
interface PreviousResults {
|
||||
taData: Map;
|
||||
beatsaverData: BeatSaverMap;
|
||||
scores: ScoreWithAccuracy[];
|
||||
completionType: 'Completed' | 'Still Awaiting Scores' | 'Exited To Menu';
|
||||
}
|
||||
import { type ScoreWithAccuracy, type PreviousResults, MapDifficulty } from '$lib/interfaces/match.interfaces';
|
||||
|
||||
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();
|
||||
|
||||
enum MapDifficulty {
|
||||
"Easy" = 0,
|
||||
"Normal" = 1,
|
||||
"Hard" = 2,
|
||||
"Expert" = 3,
|
||||
"ExpertPlus" = 4,
|
||||
}
|
||||
|
||||
const modifierNameMap: Record<number, string> = {
|
||||
[GameplayModifiers_GameOptions.None]: "",
|
||||
[GameplayModifiers_GameOptions.NoFail]: "No Fail",
|
||||
|
|
@ -87,7 +57,7 @@
|
|||
|
||||
discordMessage += `**${positionText} ${displayName}**\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`;
|
||||
});
|
||||
|
||||
|
|
@ -162,32 +132,6 @@
|
|||
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>
|
||||
|
||||
<div class="previously-played">
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@
|
|||
dispatch('removeMap', { map: map });
|
||||
}
|
||||
|
||||
function handleAddMapsFromTAPool() {
|
||||
dispatch('addMapsFromTAPool');
|
||||
}
|
||||
|
||||
function handleLoadMap(map: CustomMap) {
|
||||
mapToLoad = map;
|
||||
showConfirmDialog = true;
|
||||
|
|
@ -121,6 +125,10 @@
|
|||
<span class="material-icons">add</span>
|
||||
Add Map
|
||||
</button>
|
||||
<button class="action-button add-map" on:click={handleAddMapsFromTAPool}>
|
||||
<span class="material-icons">add</span>
|
||||
Add Maps From TA Pool
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if maps.length === 0}
|
||||
|
|
@ -337,8 +345,8 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
/* max-height: 60vh;
|
||||
overflow-y: auto; */
|
||||
}
|
||||
|
||||
.map-card {
|
||||
|
|
@ -608,7 +616,7 @@
|
|||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
/* Scrollbar Styling
|
||||
.maps-list::-webkit-scrollbar {
|
||||
width: 0.375rem;
|
||||
}
|
||||
|
|
@ -625,7 +633,7 @@
|
|||
|
||||
.maps-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
} */
|
||||
|
||||
/* Responsive Design */
|
||||
@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 { onMount } from "svelte";
|
||||
import type { BeatSaverMap } from "$lib/services/beatsaver";
|
||||
import { type Modifier, allGameModifiers } from "$lib/services/gamePlayModifiers";
|
||||
|
||||
export let tournamentGuid: string;
|
||||
export let mode: 'qualifier' | 'playing' = 'playing';
|
||||
|
|
@ -38,177 +39,18 @@
|
|||
// Qualifier-specific settings
|
||||
let limitAttemptsEnabled = false;
|
||||
let numberOfAttempts = 1;
|
||||
let disableScoresaberSubmission = false;
|
||||
let showScoreboard = false;
|
||||
|
||||
// Tournament Assistant settings for both modes
|
||||
let disableFail = false;
|
||||
let disablePause = false;
|
||||
let disableScoresaberSubmission = false;
|
||||
|
||||
// Playing-specific settings
|
||||
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
|
||||
let gameModifiers: 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']
|
||||
}
|
||||
];
|
||||
let gameModifiers: Modifier[] = allGameModifiers;
|
||||
|
||||
// Initialize from existing map data
|
||||
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",
|
||||
"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 InfoPopup from "$lib/components/notifications/InfoPopup.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 type { CustomMap } from "$lib/interfaces/match.interfaces.js";
|
||||
|
||||
export let data;
|
||||
|
||||
|
|
@ -19,37 +22,7 @@
|
|||
let isLoading = false;
|
||||
let error: string | null = null;
|
||||
let poolName: string = "";
|
||||
let maps: {
|
||||
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;
|
||||
};
|
||||
let maps: CustomMap[] = [];
|
||||
|
||||
enum MapDifficulty {
|
||||
"Easy" = 0,
|
||||
|
|
@ -59,17 +32,6 @@
|
|||
"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> = {
|
||||
[GameplayModifiers_GameOptions.None]: "",
|
||||
[GameplayModifiers_GameOptions.NoFail]: "No Fail",
|
||||
|
|
@ -109,6 +71,9 @@
|
|||
let showSuccessNotification = false;
|
||||
let successMessage = "";
|
||||
|
||||
// Import maps popup
|
||||
let showLoadMapsFromFilePopup: boolean = false;
|
||||
|
||||
// Remove 'Bearer ' from the token
|
||||
if ($authTokenStore) {
|
||||
const cleanToken = $authTokenStore.replace('Bearer ', '');
|
||||
|
|
@ -134,6 +99,14 @@
|
|||
currentEditMap = null;
|
||||
}
|
||||
|
||||
function openLoadMapsFromFilePopup() {
|
||||
showLoadMapsFromFilePopup = true;
|
||||
}
|
||||
|
||||
function closeLoadMapsFromFilePopup() {
|
||||
showLoadMapsFromFilePopup = false;
|
||||
}
|
||||
|
||||
function openDeleteConfirmation(mapId: string) {
|
||||
mapToDelete = mapId;
|
||||
showDeleteConfirmation = true;
|
||||
|
|
@ -148,6 +121,21 @@
|
|||
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) {
|
||||
await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
|
||||
fetchMapPoolData();
|
||||
|
|
@ -331,6 +319,10 @@
|
|||
<span class="material-icons">add</span>
|
||||
Add Map
|
||||
</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}>
|
||||
<span class="material-icons">refresh</span>
|
||||
Refresh
|
||||
|
|
@ -446,8 +438,7 @@
|
|||
{#if showEditMapModal}
|
||||
<EditMapModal
|
||||
tournamentGuid={tournamentGuid}
|
||||
poolGuid={poolGuid}
|
||||
map={maps.find(map => map.taData.guid == currentEditMap?.guid) || null}
|
||||
map={maps.filter(map => map.taData.guid == currentEditMap?.guid)[0] || null}
|
||||
on:close={closeEditMapModal}
|
||||
on:mapUpdated={handleMapUpdated}
|
||||
on:mapAdded={handleMapAdded}
|
||||
|
|
@ -455,6 +446,13 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Add Maps From File Modal Component -->
|
||||
<AddMapsFromFile
|
||||
isOpen={showLoadMapsFromFilePopup}
|
||||
on:close={closeLoadMapsFromFilePopup}
|
||||
on:addMaps={handleAddMapsFromFile}
|
||||
/>
|
||||
|
||||
<!-- Info Popup -->
|
||||
{#if showInfoPopup}
|
||||
<InfoPopup content={infoPopupContent} onClose={closeInfoPopup} />
|
||||
|
|
|
|||
|
|
@ -4,43 +4,51 @@
|
|||
import { bkAPIUrl } from '$lib/config.json';
|
||||
import Notification from '$lib/components/notifications/Popup.svelte';
|
||||
//@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 { 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 EditMap from "$lib/components/popups/EditMap.svelte";
|
||||
import Popup from "$lib/components/notifications/Popup.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;
|
||||
|
||||
interface CustomMap {
|
||||
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[] = [];
|
||||
|
||||
// Route params
|
||||
const tournamentGuid: string = data.tournamentGuid;
|
||||
const matchGuid: string = data.matchGuid;
|
||||
|
||||
// Core state
|
||||
let tournament: Tournament | undefined;
|
||||
let match: Match | undefined;
|
||||
let isLoading = false;
|
||||
|
|
@ -51,29 +59,11 @@
|
|||
let isMatchActive = false;
|
||||
let streamSyncEnabled = false;
|
||||
|
||||
// Song queue and results
|
||||
let upcomingQueueMaps: CustomMap[] = [];
|
||||
let realTimeScores: RealTimeScoreForPlayers[] = [];
|
||||
let previousMatchResults: 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;
|
||||
}
|
||||
let showingResultsData: PreviousResults;
|
||||
|
||||
// Modal states
|
||||
let showKickConfirmModal = false;
|
||||
|
|
@ -86,30 +76,60 @@
|
|||
let modalIconColor: ColorPreset = 'error';
|
||||
let modalButtons: ButtonConfig[] = [];
|
||||
let modalCustomImage: string = "";
|
||||
let modalAutoClose: number = 0; // no auto close
|
||||
let modalAutoClose: number = 0;
|
||||
|
||||
// Map editing states
|
||||
let showMapModal: boolean = false;
|
||||
let currentlyEditingMap: CustomMap | null = null;
|
||||
|
||||
// Song loading states
|
||||
let loadingSongForPlayers: 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"
|
||||
|
||||
// Remove 'Bearer ' from the token
|
||||
if($authTokenStore) {
|
||||
// Reactive statements
|
||||
$: isAnyPlayerPlaying = matchPlayers.some(player => (player.playState as any) === User_PlayStates.InGame);
|
||||
|
||||
if ($authTokenStore) {
|
||||
const cleanToken = $authTokenStore.replace('Bearer ', '');
|
||||
client.setAuthToken(cleanToken);
|
||||
}
|
||||
|
||||
$: isAnyPlayerPlaying = matchPlayers.some(player => (player.playState as any) === User_PlayStates.InGame);
|
||||
|
||||
// Show notification function (you mentioned you have this)
|
||||
// Show notification function
|
||||
function showNotification(message: string, type: 'warning' | 'error' | 'success' = 'warning') {
|
||||
// Your existing notification function
|
||||
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
|
||||
async function handleBackToMatches(endMatch: boolean) {
|
||||
if(match!.leader !== client.stateManager.getSelfGuid()) {
|
||||
|
|
@ -344,11 +364,74 @@
|
|||
|
||||
if(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) {
|
||||
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() {
|
||||
|
|
@ -399,6 +482,9 @@
|
|||
async function startSongForMatch() {
|
||||
if (!currentSong) return;
|
||||
|
||||
showResultsPopup = false;
|
||||
dontShowResultsPopupForThisSong = false;
|
||||
|
||||
const levelId = currentSong.beatmap.levelId;
|
||||
const playingPlayerIds = matchPlayers.map(player => player.guid);
|
||||
|
||||
|
|
@ -454,6 +540,8 @@
|
|||
|
||||
const player = matchPlayers.find(x => x.guid === rts.userGuid)!;
|
||||
|
||||
if(!player) return;
|
||||
|
||||
const index = realTimeScores.findIndex(x => x.player.guid === rts.userGuid);
|
||||
|
||||
if (index !== -1) {
|
||||
|
|
@ -495,6 +583,39 @@
|
|||
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) {
|
||||
if (rts.matchId !== matchGuid) return;
|
||||
|
||||
|
|
@ -513,7 +634,8 @@
|
|||
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
|
||||
const playersStillPlaying = Array.from(activeSongPlayers)
|
||||
|
|
@ -551,6 +673,9 @@
|
|||
...previousMatchResults.filter((_, i) => i !== existingRecordIndex)
|
||||
];
|
||||
}
|
||||
|
||||
showingResultsData = previousMatchResults[0];
|
||||
showResultsPopup = true;
|
||||
}
|
||||
|
||||
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() {
|
||||
showMapModal = false;
|
||||
}
|
||||
|
|
@ -606,6 +748,7 @@
|
|||
const map: CustomMap = event.detail.map;
|
||||
loadingSongForPlayers = false;
|
||||
failedToLoadSongForPlayers = false;
|
||||
dontShowResultsPopupForThisSong = false;
|
||||
|
||||
let playingPlayerIds = matchPlayers
|
||||
.filter(player => player.playState !== User_PlayStates.InGame)
|
||||
|
|
@ -704,7 +847,7 @@
|
|||
End Match
|
||||
</button>
|
||||
<div class="match-info">
|
||||
<h2>Match: {'Unknown'}</h2>
|
||||
<h2>Match: {match?.guid || 'Undefined'}</h2>
|
||||
<p class="tournament-name">{tournament?.settings?.tournamentName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -865,6 +1008,7 @@
|
|||
on:editMap={handleEditMap}
|
||||
on:removeMap={handleRemoveMapFromQueue}
|
||||
on:songLoad={handleLoadMap}
|
||||
on:addMapsFromTAPool={handleShowAddMapsFromTAPool}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -872,6 +1016,7 @@
|
|||
<div class="queue-section">
|
||||
<PreviouslyPlayedSongs
|
||||
maps={previousMatchResults}
|
||||
on:showMapDetails={showResultsPopupWithOldScores}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -901,6 +1046,21 @@
|
|||
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>
|
||||
.match-dashboard {
|
||||
display: flex;
|
||||
|
|
@ -1048,6 +1208,8 @@
|
|||
border-radius: 0.75rem;
|
||||
padding-top: 1rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
max-height: 20rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
|
|
@ -1111,8 +1273,8 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.player-card {
|
||||
|
|
|
|||
Loading…
Reference in a new issue