add modifiers

This commit is contained in:
Luna 2025-08-22 20:15:44 +02:00
parent 8602b60b02
commit 80f455f1c7
7 changed files with 578 additions and 28 deletions

8
package-lock.json generated
View file

@ -16,7 +16,7 @@
"d3": "^7.9.0",
"file-saver": "^2.0.5",
"marked": "^14.1.4",
"moons-ta-client": "^1.2.0",
"moons-ta-client": "^1.2.1",
"pako": "^2.1.0",
"prismjs": "^1.30.0",
"uuid": "^11.1.0",
@ -3235,9 +3235,9 @@
}
},
"node_modules/moons-ta-client": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/moons-ta-client/-/moons-ta-client-1.2.0.tgz",
"integrity": "sha512-QB/rXVrA4/2JOybg1pVB1qVHH4n3ZbtbQuTi2VpC8+TQLDgOcpbqQE+GsEof6PD2/utIwau+e/qlJQm8bcE2Fw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/moons-ta-client/-/moons-ta-client-1.2.1.tgz",
"integrity": "sha512-oADroR3opRRb0HCja3WbQ3LyXd2hMx9vHnTwDAo4TYm3kc718fiSWSTOIk2G3RZ9dM94fu2t6fT03rbNU8EInw==",
"dependencies": {
"@protobuf-ts/plugin": "^2.6.0",
"events": "^3.3.0",

View file

@ -20,7 +20,7 @@
"d3": "^7.9.0",
"file-saver": "^2.0.5",
"marked": "^14.1.4",
"moons-ta-client": "^1.2.0",
"moons-ta-client": "^1.2.1",
"pako": "^2.1.0",
"prismjs": "^1.30.0",
"uuid": "^11.1.0",

View file

@ -0,0 +1,427 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
// Props
export let playerPfp: string;
export let playerName: string;
export let playerPlatformId: string;
export let isVisible: boolean = false;
// Enum definition (matching your TypeScript enum)
enum Command_ModifyGameplay_Modifier {
InvertColors = 0,
InvertHandedness = 1,
DisableBlueNotes = 2,
DisableRedNotes = 3
}
// Event dispatcher
const dispatch = createEventDispatcher();
// Local state for toggles (all start as false/off)
let invertColors = false;
let invertHandedness = false;
let disableBlueNotes = false;
let disableRedNotes = false;
// Calculate bitwise modifiers
function calculateModifiers(): number {
let modifiers = 0;
if (invertColors) modifiers |= (1 << Command_ModifyGameplay_Modifier.InvertColors);
if (invertHandedness) modifiers |= (1 << Command_ModifyGameplay_Modifier.InvertHandedness);
if (disableBlueNotes) modifiers |= (1 << Command_ModifyGameplay_Modifier.DisableBlueNotes);
if (disableRedNotes) modifiers |= (1 << Command_ModifyGameplay_Modifier.DisableRedNotes);
return modifiers;
}
// Handle apply modifiers
function handleApply() {
const modifiers = calculateModifiers();
dispatch('sendModifyGameplay', {
platformId: playerPlatformId,
modifiers: modifiers
});
isVisible = false;
}
// Handle cancel/close
function handleClose() {
isVisible = false;
}
// Handle backdrop click
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
handleClose();
}
}
// Handle escape key
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
handleClose();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if isVisible}
<!-- Backdrop -->
<div
class="popup-backdrop"
on:click={handleBackdropClick}
on:keydown={handleKeydown}
role="button"
tabindex="-1"
>
<!-- Popup Container -->
<div class="popup-container">
<!-- Header -->
<div class="popup-header">
<button class="close-button" on:click={handleClose} aria-label="Close">
<span class="material-icons">close</span>
</button>
<h2>Modify Gameplay</h2>
</div>
<!-- Player Info -->
<div class="player-info">
<img src={playerPfp} alt={playerName} class="player-avatar" />
<div class="player-details">
<div class="player-name">{playerName}</div>
<div class="player-id">ID: {playerPlatformId}</div>
</div>
</div>
<!-- Modifiers Section -->
<div class="modifiers-section">
<h3>Gameplay Modifiers</h3>
<!-- Invert Colors Toggle -->
<div class="modifier-item">
<div class="modifier-info">
<span class="material-icons modifier-icon">palette</span>
<div class="modifier-text">
<div class="modifier-title">Invert Colors</div>
<div class="modifier-description">Swap red and blue colors</div>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox" bind:checked={invertColors} />
<span class="toggle-slider"></span>
</label>
</div>
<!-- Invert Handedness Toggle -->
<div class="modifier-item">
<div class="modifier-info">
<span class="material-icons modifier-icon">swap_horiz</span>
<div class="modifier-text">
<div class="modifier-title">Invert Hands</div>
<div class="modifier-description">Swap left and right hand controls</div>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox" bind:checked={invertHandedness} />
<span class="toggle-slider"></span>
</label>
</div>
<!-- Disable Blue Notes Toggle -->
<div class="modifier-item">
<div class="modifier-info">
<span class="material-icons modifier-icon" style="color: #4285f4;">block</span>
<div class="modifier-text">
<div class="modifier-title">Disable Blue (Right) Notes</div>
<div class="modifier-description">Only red (left) notes will appear</div>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox" bind:checked={disableBlueNotes} />
<span class="toggle-slider"></span>
</label>
</div>
<!-- Disable Red Notes Toggle -->
<div class="modifier-item">
<div class="modifier-info">
<span class="material-icons modifier-icon" style="color: #ea4335;">block</span>
<div class="modifier-text">
<div class="modifier-title">Disable Red (Left) Notes</div>
<div class="modifier-description">Only blue (right) notes will appear</div>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox" bind:checked={disableRedNotes} />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="cancel-button" on:click={handleClose}>
<span class="material-icons">cancel</span>
Cancel
</button>
<button class="apply-button" on:click={handleApply}>
<span class="material-icons">check_circle</span>
Apply Modifiers
</button>
</div>
</div>
</div>
{/if}
<style>
.popup-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
.popup-container {
background-color: #1a1a1a;
border-radius: 16px;
border: 1px solid #333;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
animation: popupAppear 0.2s ease-out;
}
@keyframes popupAppear {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.popup-header {
display: flex;
align-items: center;
padding: 20px 24px 16px 24px;
border-bottom: 1px solid #333;
position: relative;
}
.close-button {
position: absolute;
left: 24px;
background: none;
border: none;
color: #999;
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #fff;
background-color: #333;
}
.popup-header h2 {
flex: 1;
text-align: center;
margin: 0;
color: #fff;
font-size: 20px;
font-weight: 600;
}
.player-info {
display: flex;
align-items: center;
padding: 20px 24px;
gap: 16px;
border-bottom: 1px solid #333;
}
.player-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid #444;
}
.player-details {
flex: 1;
}
.player-name {
color: #fff;
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.player-id {
color: #999;
font-size: 12px;
}
.modifiers-section {
padding: 24px;
}
.modifiers-section h3 {
color: #fff;
margin: 0 0 20px 0;
font-size: 16px;
font-weight: 600;
}
.modifier-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid #2a2a2a;
}
.modifier-item:last-child {
border-bottom: none;
}
.modifier-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.modifier-icon {
color: #999;
font-size: 20px;
}
.modifier-text {
flex: 1;
}
.modifier-title {
color: #fff;
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
}
.modifier-description {
color: #999;
font-size: 12px;
line-height: 1.3;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #333;
transition: 0.2s ease;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: #999;
transition: 0.2s ease;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #4285f4;
}
input:checked + .toggle-slider:before {
transform: translateX(20px);
background-color: #fff;
}
.action-buttons {
display: flex;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #333;
}
.cancel-button,
.apply-button {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 20px;
border-radius: 8px;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.cancel-button {
background-color: #333;
color: #ccc;
}
.cancel-button:hover {
background-color: #444;
color: #fff;
}
.apply-button {
background-color: #4285f4;
color: #fff;
}
.apply-button:hover {
background-color: #3367d6;
}
.material-icons {
font-size: 18px;
}
</style>

View file

@ -12,14 +12,10 @@ export interface RealTimeScoreForPlayers {
recentScore: RealtimeScore;
}
export interface ScoreWithAccuracy extends Push_SongFinished {
accuracy: string;
}
export interface PreviousResults {
taData: Map;
beatsaverData: BeatSaverMap;
scores: ScoreWithAccuracy[];
scores: Push_SongFinished[];
completionType: 'Completed' | 'Still Awaiting Scores' | 'Exited To Menu';
}
@ -56,6 +52,6 @@ export interface ButtonConfig {
export interface CustomTAMapPool {
guid: string;
name: string;
image: Uint8Array;
image: string;
maps: CustomMap[];
}

View file

@ -34,7 +34,10 @@ export function arrayBufferToBase64(buffer: Uint8Array): string {
return btoa(binary);
}
export async function convertImageToUint8Array(file: File): Promise<Uint8Array> {
export async function convertImageToUint8Array(file: File | null): Promise<Uint8Array> {
if(!file) {
return new Uint8Array([1]);
}
return new Promise((resolve, reject) => {
// Create a FileReader to read the file
const reader = new FileReader();

View file

@ -14,6 +14,7 @@
name: string;
image: string;
guid: string;
myPermissions: string[];
// authorisedUsers: any;
}
@ -85,7 +86,7 @@
imageUrl = t.settings.tournamentImage;
}
// If the image is a Uint8Array
else if (t.settings.tournamentImage instanceof Uint8Array) {
else if (typeof t.settings.tournamentImage === 'object') {
// Create and properly dispose of object URLs to prevent memory leaks
try {
imageUrl = bufferToImageUrl(t.settings.tournamentImage);
@ -109,9 +110,12 @@
name: t.settings?.tournamentName || 'Unnamed Tournament',
image: imageUrl,
guid: t.guid,
myPermissions: t.settings!.myPermissions
// authorisedUsers: authorisedUsersPromise // Wait for the promise to resolve
};
}));
console.log("tournaments", tournaments)
} catch (connErr) {
console.error('TAClient connection error:', connErr);
authError = connErr instanceof Error ? connErr.message : 'Failed to connect to TA server';
@ -216,7 +220,8 @@
qualifiers: [],
settings: {
tournamentName: newTournamentName.trim(),
tournamentImage: new Uint8Array([1]), // Default placeholder
// tournamentImage: new Uint8Array([1]), // Default placeholder
tournamentImage: "",
enableTeams: false,
enablePools: false,
showTournamentButton: true,
@ -225,7 +230,9 @@
scoreUpdateFrequency: 30,
bannedMods: [],
pools: [],
allowUnauthorizedView: true
allowUnauthorizedView: true,
roles: [],
myPermissions: []
},
server: {
name: `${$TAServerUrl}:${$TAServerPort}`,
@ -238,7 +245,10 @@
// Convert image to Uint8Array if an image was provided
if (newTournamentImage) {
try {
tournament.settings!.tournamentImage = await convertImageToUint8Array(newTournamentImage);
// old way of making an image empty in TA, changed in fileserver update
// tournament.settings!.tournamentImage = await convertImageToUint8Array(newTournamentImage);
// new way is just an empty string
tournament.settings!.tournamentImage = "";
console.log("Image converted successfully", tournament.settings!.tournamentImage.length, "bytes");
} catch (error) {
console.error("Failed to convert image:", error);
@ -248,8 +258,22 @@
// Create the tournament
console.log("Creating tournament:", tournament);
const response = await client.createTournament(tournament);
const response = await client.createTournament(
tournament.server!.address,
tournament.server!.name,
tournament.server!.port.toString(),
tournament.server!.websocketPort.toString(),
tournament.settings!.tournamentName,
await convertImageToUint8Array(newTournamentImage),
false,
false,
true,
true,
);
console.log("Tournament created:", response);
const responseTournament = (response.details as any).tournament;
// Add the new tournament to the list (for immediate display)
let imageUrl = '/images/tournaments/default.jpg'; // Default fallback
@ -260,10 +284,11 @@
tournaments = [
...tournaments,
{
id: tournament.guid.substring(0, 8),
name: tournament.settings!.tournamentName,
id: responseTournament.guid.substring(0, 8),
name: responseTournament.settings!.tournamentName,
image: imageUrl,
guid: tournament.guid,
guid: responseTournament.guid,
myPermissions: responseTournament.settings!.myPermissions
// authorisedUsers: []
}
];

View file

@ -15,7 +15,10 @@
Map,
User_DownloadStates,
User_PlayStates,
Tournament_TournamentSettings_Pool
Tournament_TournamentSettings_Pool,
Command_ModifyGameplay_Modifier
} from 'moons-ta-client';
import { writable } from "svelte/store";
import { goto } from "$app/navigation";
@ -26,12 +29,12 @@
import PreviouslyPlayedSongs from "$lib/components/modules/PreviouslyPlayedSongs.svelte";
import ResultsForMap from "$lib/components/popups/ResultsForMap.svelte";
import AddMapsFromTAPool from "$lib/components/popups/AddMapsFromTAPool.svelte";
import ModifyGameplay from "$lib/components/popups/ModifyGameplay.svelte";
// Types
import {
type CustomMap,
type RealTimeScoreForPlayers,
type ScoreWithAccuracy,
type PreviousResults,
type ButtonConfig,
type ColorPreset,
@ -97,6 +100,10 @@
let showAddFromTAPoolPopup: boolean = false;
let allowAddFromTAPool: boolean = true;
// User editing state
let isModifyGameplayPopupVisible: boolean = false;
let editingPlayerPlatformId: string, editingPlayerName: string, editingPlayerPfp: string = "";
// Active song tracking
const activeSongPlayers = new Set<`${string}-${string}`>(); // Format: "userGuid-levelId"
@ -616,14 +623,14 @@
}
// Main accuracy calculation logic
function calculateAccuracy(score: Push_SongFinished, mapWithSongInfo: CustomMap): string {
function calculateAccuracy(score: Push_SongFinished, mapWithSongInfo: CustomMap): number {
const maxScore = getMaxScore(
mapWithSongInfo.beatsaverData,
score.beatmap?.characteristic?.serializedName ?? "Standard",
getDifficultyAsString(score.beatmap?.difficulty ?? 4) || "ExpertPlus",
);
const accuracy = ((score.score / maxScore) * 100).toFixed(2);
const accuracy = parseFloat(((score.score / maxScore) * 100).toFixed(2));
return accuracy;
}
@ -645,7 +652,7 @@
return;
}
let newRts: ScoreWithAccuracy = {...rts, accuracy: "0"};
let newRts: Push_SongFinished = {...rts, accuracy: 0};
newRts.accuracy = calculateAccuracy(rts, map);
// Check if any players are still playing this song
@ -806,6 +813,65 @@
}
}
async function handleSetGameplayModifiersForUsersVerbose(userIds: string[], modifiers: number) {
try {
const results: any[] = [];
// Helper function to check if a specific modifier is enabled
const isModifierEnabled = (modifier: Command_ModifyGameplay_Modifier): boolean => {
return (modifiers & (1 << modifier)) !== 0;
};
// Apply each modifier if enabled
if (isModifierEnabled(Command_ModifyGameplay_Modifier.InvertColors)) {
console.log('Inverting colors for users:', userIds);
const response = await client.flipColors(tournamentGuid, userIds);
results.push({ type: 'InvertColors', response });
}
if (isModifierEnabled(Command_ModifyGameplay_Modifier.InvertHandedness)) {
console.log('Inverting handedness for users:', userIds);
const response = await client.flipHands(tournamentGuid, userIds);
results.push({ type: 'InvertHandedness', response });
}
if (isModifierEnabled(Command_ModifyGameplay_Modifier.DisableBlueNotes)) {
console.log('Disabling blue notes for users:', userIds);
const response = await client.disableBlueNotes(tournamentGuid, userIds);
results.push({ type: 'DisableBlueNotes', response });
}
if (isModifierEnabled(Command_ModifyGameplay_Modifier.DisableRedNotes)) {
console.log('Disabling red notes for users:', userIds);
const response = await client.disableRedNotes(tournamentGuid, userIds);
results.push({ type: 'DisableRedNotes', response });
}
console.log(`Applied ${results.length} modifiers successfully`);
return results;
} catch (error) {
console.error('Error applying gameplay modifiers:', error);
throw error;
}
}
async function handleModifyGameplayForPlayer(player: User) {
editingPlayerName = player.name;
editingPlayerPfp = player.discordInfo?.avatarUrl || "";
editingPlayerPlatformId = player.platformId;
isModifyGameplayPopupVisible = true;
}
async function handleSendModifyGameplay(event: CustomEvent) {
const { platformId, modifiers } = event.detail;
// Convert platformId to userIds array if needed
const userIds = [platformId]; // or however you map platform ID to user IDs
await handleSetGameplayModifiersForUsersVerbose(userIds, modifiers);
}
const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
event.preventDefault(); // Not necessary in some browsers but safe
};
@ -1005,9 +1071,15 @@
<p class="player-status-text">{User_DownloadStates[player.downloadState]}</p>
<p class="player-status-text">{User_PlayStates[player.playState]}</p>
</div>
<button class="kick-button" on:click={() => handleKickPlayer(player)}>
<span class="material-icons">person_remove</span>
</button>
<div style="display: inline-block; margin-right: 0.4rem;">
<button class="kick-button" on:click={() => handleKickPlayer(player)}>
<span class="material-icons">person_remove</span>
</button>
<button class="modify-button" on:click={() => handleModifyGameplayForPlayer(player)}>
<span class="material-icons">settings</span>
</button>
</div>
</div>
{/each}
</div>
@ -1075,6 +1147,13 @@
/>
{/if}
<ModifyGameplay
bind:isVisible={isModifyGameplayPopupVisible}
playerName={editingPlayerName}
playerPfp={editingPlayerPfp}
playerPlatformId={editingPlayerPlatformId}
/>
<style>
.match-dashboard {
display: flex;
@ -1349,6 +1428,26 @@
background-color: var(--danger-hover);
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
}
.modify-button {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.modify-button:hover {
background-color: var(--accent-hover);
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
}
/* Empty State */
.empty-state {