usuarios pre configurados
This commit is contained in:
@@ -8,24 +8,35 @@ const router = createRouter({
|
|||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/:uuid',
|
||||||
name: 'Lobby',
|
name: 'LobbyWithUuid',
|
||||||
component: Lobby
|
component: Lobby
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/game',
|
path: '/:uuid/game',
|
||||||
name: 'Game',
|
name: 'GameWithUuid',
|
||||||
component: Game
|
component: Game
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/demo',
|
path: '/:uuid/demo',
|
||||||
name: 'DemoGame',
|
name: 'DemoGameWithUuid',
|
||||||
component: DemoGame
|
component: DemoGame
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
component: Dashboard
|
component: Dashboard
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/missing-uuid'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// simple fallback for users hitting root without UUID
|
||||||
|
path: '/missing-uuid',
|
||||||
|
component: {
|
||||||
|
template: `<div style="padding:20px;font-family:sans-serif"><h2>Falta UUID</h2><p>Abre el juego escaneando tu código QR: snatchgame.nucleoriofrio.com/{uuid}</p></div>`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Client, Room } from "colyseus.js";
|
import { Client, Room } from "colyseus.js";
|
||||||
import { ref, Ref } from "vue";
|
import { ref, Ref } from "vue";
|
||||||
import { localDB } from "./db";
|
|
||||||
|
|
||||||
export interface PlayerData {
|
export interface PlayerData {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -25,8 +24,8 @@ class ColyseusService {
|
|||||||
private client: Client;
|
private client: Client;
|
||||||
private currentRoom: Room | null = null;
|
private currentRoom: Room | null = null;
|
||||||
private apiBase: string;
|
private apiBase: string;
|
||||||
private readonly LS_KEY_RECONNECT = "snatch.game.rtoken";
|
// No local storage tokens: UUID in URL identifies the user
|
||||||
|
|
||||||
public lobbyRoom: Ref<Room | null> = ref(null);
|
public lobbyRoom: Ref<Room | null> = ref(null);
|
||||||
public gameRoom: Ref<Room | null> = ref(null);
|
public gameRoom: Ref<Room | null> = ref(null);
|
||||||
public playerName: Ref<string> = ref("");
|
public playerName: Ref<string> = ref("");
|
||||||
@@ -50,10 +49,8 @@ class ColyseusService {
|
|||||||
|
|
||||||
async joinLobby(): Promise<Room> {
|
async joinLobby(): Promise<Room> {
|
||||||
try {
|
try {
|
||||||
// Initialize DB first to get UUID
|
const uuid = this.getUuidFromPath();
|
||||||
await localDB.init();
|
|
||||||
const uuid = localDB.getUUID();
|
|
||||||
|
|
||||||
const room = await this.client.joinOrCreate("lobby", { uuid });
|
const room = await this.client.joinOrCreate("lobby", { uuid });
|
||||||
this.lobbyRoom.value = room;
|
this.lobbyRoom.value = room;
|
||||||
this.currentRoom = room;
|
this.currentRoom = room;
|
||||||
@@ -69,36 +66,18 @@ class ColyseusService {
|
|||||||
this.playerName.value = data.name;
|
this.playerName.value = data.name;
|
||||||
this.nameConfirmed.value = true;
|
this.nameConfirmed.value = true;
|
||||||
} else {
|
} else {
|
||||||
// Initialize local DB and optionally auto-apply saved profile
|
// No client-side persistence; user must confirm name once per session
|
||||||
try {
|
|
||||||
await localDB.init();
|
|
||||||
const profile = localDB.getLocalPlayer();
|
|
||||||
// Apply saved color silently
|
|
||||||
if (profile?.color && profile.color !== this.playerColor.value) {
|
|
||||||
this.setPlayerColor(profile.color);
|
|
||||||
}
|
|
||||||
if (profile?.name) {
|
|
||||||
this.playerName.value = profile.name;
|
|
||||||
try { localDB.setName(profile.name); } catch {}
|
|
||||||
this.setPlayerName(profile.name);
|
|
||||||
this.nameConfirmed.value = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Local DB init failed", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("nameUpdated", (data) => {
|
room.onMessage("nameUpdated", (data) => {
|
||||||
this.playerName.value = data.name;
|
this.playerName.value = data.name;
|
||||||
try { localDB.setName(data.name); } catch {}
|
|
||||||
this.nameConfirmed.value = true;
|
this.nameConfirmed.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("colorUpdated", (data) => {
|
room.onMessage("colorUpdated", (data) => {
|
||||||
if (data?.color) {
|
if (data?.color) {
|
||||||
this.playerColor.value = data.color;
|
this.playerColor.value = data.color;
|
||||||
try { localDB.setColor(data.color); } catch {}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,28 +89,13 @@ class ColyseusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryReconnectToOngoingGame(): Promise<Room | null> {
|
async tryReconnectToOngoingGame(): Promise<Room | null> {
|
||||||
try {
|
// No stored reconnection tokens; rely on UUID flow and fresh joins
|
||||||
const token = typeof window !== 'undefined' ? (window.localStorage.getItem(this.LS_KEY_RECONNECT) || "") : "";
|
return Promise.resolve(null);
|
||||||
if (!token) return null;
|
|
||||||
const room = await this.client.reconnect(token);
|
|
||||||
this.gameRoom.value = room;
|
|
||||||
this.currentRoom = room;
|
|
||||||
// Ensure local session id reflects the active room session
|
|
||||||
try { this.sessionId.value = (room as any).sessionId || this.sessionId.value; } catch {}
|
|
||||||
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_RECONNECT, (room as any).reconnectionToken || token); } catch {}
|
|
||||||
return room;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Reconnection failed, clearing tokens');
|
|
||||||
try {
|
|
||||||
if (typeof window !== 'undefined') { window.localStorage.removeItem(this.LS_KEY_RECONNECT); }
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPlayerName(name: string): Promise<void> {
|
async setPlayerName(name: string): Promise<void> {
|
||||||
if (this.lobbyRoom.value) {
|
if (this.lobbyRoom.value) {
|
||||||
const uuid = localDB.getUUID();
|
const uuid = this.getUuidFromPath();
|
||||||
this.lobbyRoom.value.send("setName", { name, uuid });
|
this.lobbyRoom.value.send("setName", { name, uuid });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,9 +120,11 @@ class ColyseusService {
|
|||||||
try {
|
try {
|
||||||
// Join the game room directly using the roomId
|
// Join the game room directly using the roomId
|
||||||
console.log('Joining game room with name:', this.playerName.value);
|
console.log('Joining game room with name:', this.playerName.value);
|
||||||
|
const uuid = this.getUuidFromPath();
|
||||||
const gameRoom = await this.client.joinById(data.roomId, {
|
const gameRoom = await this.client.joinById(data.roomId, {
|
||||||
playerName: this.playerName.value,
|
playerName: this.playerName.value,
|
||||||
playerColor: this.playerColor.value
|
playerColor: this.playerColor.value,
|
||||||
|
uuid
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure the room id is set
|
// Ensure the room id is set
|
||||||
@@ -172,7 +138,6 @@ class ColyseusService {
|
|||||||
this.currentRoom = gameRoom;
|
this.currentRoom = gameRoom;
|
||||||
// Update current session id for correct role mapping
|
// Update current session id for correct role mapping
|
||||||
try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {}
|
try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {}
|
||||||
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_RECONNECT, (gameRoom as any).reconnectionToken || ""); } catch {}
|
|
||||||
console.log('gameRoom.value is now:', this.gameRoom.value);
|
console.log('gameRoom.value is now:', this.gameRoom.value);
|
||||||
|
|
||||||
// Don't register message handlers here - let the Game component handle them
|
// Don't register message handlers here - let the Game component handle them
|
||||||
@@ -198,12 +163,13 @@ class ColyseusService {
|
|||||||
|
|
||||||
async joinGameRoom(roomId: string): Promise<Room> {
|
async joinGameRoom(roomId: string): Promise<Room> {
|
||||||
try {
|
try {
|
||||||
|
const uuid = this.getUuidFromPath();
|
||||||
const gameRoom = await this.client.joinById(roomId, {
|
const gameRoom = await this.client.joinById(roomId, {
|
||||||
playerName: this.playerName.value,
|
playerName: this.playerName.value,
|
||||||
playerColor: this.playerColor.value
|
playerColor: this.playerColor.value,
|
||||||
|
uuid
|
||||||
});
|
});
|
||||||
try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {}
|
try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {}
|
||||||
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_RECONNECT, (gameRoom as any).reconnectionToken || ""); } catch {}
|
|
||||||
this.gameRoom.value = gameRoom;
|
this.gameRoom.value = gameRoom;
|
||||||
this.currentRoom = gameRoom;
|
this.currentRoom = gameRoom;
|
||||||
|
|
||||||
@@ -291,7 +257,6 @@ class ColyseusService {
|
|||||||
this.currentRoom = null;
|
this.currentRoom = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try { if (typeof window !== 'undefined') { window.localStorage.removeItem(this.LS_KEY_RECONNECT); } } catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveCurrentRoom(): void {
|
leaveCurrentRoom(): void {
|
||||||
@@ -350,6 +315,13 @@ class ColyseusService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUuidFromPath(): string {
|
||||||
|
if (typeof window === 'undefined') return '';
|
||||||
|
const path = window.location.pathname.replace(/^\/+/, '');
|
||||||
|
const seg = path.split('/')[0] || '';
|
||||||
|
return seg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const colyseusService = new ColyseusService();
|
export const colyseusService = new ColyseusService();
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { colyseusService } from '../services/colyseus';
|
import { colyseusService } from '../services/colyseus';
|
||||||
import { getStateCallbacks } from 'colyseus.js';
|
import { getStateCallbacks } from 'colyseus.js';
|
||||||
|
|
||||||
@@ -81,6 +81,8 @@ import PlayerStats from './games/PlayerStats.vue';
|
|||||||
import ChatWidget from './games/ChatWidget.vue';
|
import ChatWidget from './games/ChatWidget.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const routeUuid = computed(() => (route.params as any)?.uuid as string || '');
|
||||||
|
|
||||||
const players = ref<any[]>([]);
|
const players = ref<any[]>([]);
|
||||||
const gameStatus = ref('waiting');
|
const gameStatus = ref('waiting');
|
||||||
@@ -126,14 +128,7 @@ const currentComponent = computed(() => componentMap[currentVariant.value]);
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let room = colyseusService.gameRoom.value;
|
let room = colyseusService.gameRoom.value;
|
||||||
if (!room) {
|
if (!room) {
|
||||||
// Try reconnection first
|
router.push(`/${routeUuid.value}`);
|
||||||
colyseusService.tryReconnectToOngoingGame().then(r => {
|
|
||||||
if (!r) {
|
|
||||||
router.push('/');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setupRoom(r);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +176,7 @@ onMounted(() => {
|
|||||||
$(player).listen("shameTokens", (v: number) => { const p = players.value.find(x => x.sessionId === key); if (p) p.shameTokens = v; });
|
$(player).listen("shameTokens", (v: number) => { const p = players.value.find(x => x.sessionId === key); if (p) p.shameTokens = v; });
|
||||||
$(player).listen("color", (v: string) => { const p = players.value.find(x => x.sessionId === key); if (p) p.color = v; });
|
$(player).listen("color", (v: string) => { const p = players.value.find(x => x.sessionId === key); if (p) p.color = v; });
|
||||||
});
|
});
|
||||||
$(room.state).players.onRemove((player: any, key: string) => {
|
$(room.state).players.onRemove((_: any, key: string) => {
|
||||||
const i = players.value.findIndex(p => p.sessionId === key);
|
const i = players.value.findIndex(p => p.sessionId === key);
|
||||||
if (i !== -1) players.value.splice(i, 1);
|
if (i !== -1) players.value.splice(i, 1);
|
||||||
});
|
});
|
||||||
@@ -192,7 +187,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("gameEnd", () => {
|
room.onMessage("gameEnd", () => {
|
||||||
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.rtoken'); } } catch {}
|
// no-op for local storage
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register additional message handlers to avoid warnings
|
// Register additional message handlers to avoid warnings
|
||||||
@@ -209,7 +204,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle room closure/disconnection
|
// Handle room closure/disconnection
|
||||||
room.onLeave((code) => {
|
room.onLeave((code: number) => {
|
||||||
console.log('[DemoGame] Room disconnected with code:', code);
|
console.log('[DemoGame] Room disconnected with code:', code);
|
||||||
|
|
||||||
// Handle shuffle disconnection specially
|
// Handle shuffle disconnection specially
|
||||||
@@ -223,36 +218,25 @@ onMounted(() => {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Redirect to lobby and let it handle the shuffle redirect
|
// Redirect to lobby and let it handle the shuffle redirect
|
||||||
router.push('/');
|
router.push(`/${routeUuid.value}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal disconnection handling
|
// Normal disconnection handling
|
||||||
// Always clean up local storage when room closes
|
// Always clean up local storage when room closes
|
||||||
try {
|
// no-op for local storage cleanup
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.localStorage.removeItem('snatch.game.roomId');
|
|
||||||
window.localStorage.removeItem('snatch.game.sessionId');
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// If not on lobby page, redirect there
|
// If not on lobby page, redirect there
|
||||||
if (router.currentRoute.value.path !== '/') {
|
if (router.currentRoute.value.path !== `/${routeUuid.value}`) {
|
||||||
console.log('[DemoGame] Room closed, redirecting to lobby');
|
console.log('[DemoGame] Room closed, redirecting to lobby');
|
||||||
router.push('/');
|
router.push(`/${routeUuid.value}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onError((code, message) => {
|
room.onError((code: number, message: any) => {
|
||||||
console.error('[DemoGame] Room error:', code, message);
|
console.error('[DemoGame] Room error:', code, message);
|
||||||
// On error, also redirect to lobby
|
// On error, redirect to lobby
|
||||||
try {
|
router.push(`/${routeUuid.value}`);
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.localStorage.removeItem('snatch.game.roomId');
|
|
||||||
window.localStorage.removeItem('snatch.game.sessionId');
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
router.push('/');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -276,8 +260,7 @@ function onAssignShame(val: boolean) { colyseusService.assignShame(val); }
|
|||||||
function leaveGame() {
|
function leaveGame() {
|
||||||
console.log('[DemoGame] User manually leaving game');
|
console.log('[DemoGame] User manually leaving game');
|
||||||
colyseusService.leaveGame();
|
colyseusService.leaveGame();
|
||||||
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.roomId'); window.localStorage.removeItem('snatch.game.sessionId'); } } catch {}
|
router.push(`/${routeUuid.value}`);
|
||||||
router.push('/');
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -75,12 +75,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { colyseusService } from '../services/colyseus';
|
import { colyseusService } from '../services/colyseus';
|
||||||
import { localDB } from '../services/db';
|
|
||||||
import { getStateCallbacks } from 'colyseus.js';
|
import { getStateCallbacks } from 'colyseus.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const routeUuid = computed(() => (route.params as any)?.uuid as string || '');
|
||||||
const players = ref<any[]>([]);
|
const players = ref<any[]>([]);
|
||||||
const gameStatus = ref('waiting');
|
const gameStatus = ref('waiting');
|
||||||
const timeRemaining = ref(600);
|
const timeRemaining = ref(600);
|
||||||
@@ -99,16 +100,8 @@ onMounted(() => {
|
|||||||
console.log('Current game room:', room);
|
console.log('Current game room:', room);
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.warn('No game room found, trying reconnection...');
|
console.warn('No game room found, redirecting to lobby...');
|
||||||
// Attempt reconnection if tokens are available
|
router.push(`/${routeUuid.value}`);
|
||||||
colyseusService.tryReconnectToOngoingGame().then((r) => {
|
|
||||||
if (!r) {
|
|
||||||
console.error('Reconnection failed, redirecting to lobby...');
|
|
||||||
router.push('/');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setupRoom(r);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +113,7 @@ onMounted(() => {
|
|||||||
const $ = getStateCallbacks(room);
|
const $ = getStateCallbacks(room);
|
||||||
|
|
||||||
// Wait for the initial state sync (do not manually push players here to avoid duplicates)
|
// Wait for the initial state sync (do not manually push players here to avoid duplicates)
|
||||||
room.onStateChange.once((state) => {
|
room.onStateChange.once((state: any) => {
|
||||||
console.log('Initial state received:', state);
|
console.log('Initial state received:', state);
|
||||||
gameStatus.value = state.gameStatus || 'waiting';
|
gameStatus.value = state.gameStatus || 'waiting';
|
||||||
timeRemaining.value = state.timeRemaining || 600;
|
timeRemaining.value = state.timeRemaining || 600;
|
||||||
@@ -164,14 +157,14 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(room.state).players.onRemove((player: any, key: string) => {
|
$(room.state).players.onRemove((_: any, key: string) => {
|
||||||
const index = players.value.findIndex(p => p.sessionId === key);
|
const index = players.value.findIndex(p => p.sessionId === key);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
players.value.splice(index, 1);
|
players.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("playerInfo", (info) => {
|
room.onMessage("playerInfo", (info: any) => {
|
||||||
console.log('Received playerInfo:', info);
|
console.log('Received playerInfo:', info);
|
||||||
colyseusService.sessionId.value = info.sessionId;
|
colyseusService.sessionId.value = info.sessionId;
|
||||||
colyseusService.playerName.value = info.name;
|
colyseusService.playerName.value = info.name;
|
||||||
@@ -182,10 +175,9 @@ onMounted(() => {
|
|||||||
gameStatus.value = 'playing';
|
gameStatus.value = 'playing';
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("gameEnd", (data) => {
|
room.onMessage("gameEnd", () => {
|
||||||
console.log("Game ended!", data);
|
console.log("Game ended!");
|
||||||
gameStatus.value = 'finished';
|
gameStatus.value = 'finished';
|
||||||
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.rtoken'); } } catch {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("gamePaused", () => {
|
room.onMessage("gamePaused", () => {
|
||||||
@@ -207,18 +199,17 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (gameStatus.value !== 'playing') return;
|
if (gameStatus.value !== 'playing') return;
|
||||||
|
|
||||||
colyseusService.sendClick();
|
colyseusService.sendClick();
|
||||||
try { localDB.incClicks(1); } catch {}
|
|
||||||
|
isClicking.value = true;
|
||||||
isClicking.value = true;
|
clearTimeout(clickTimeout);
|
||||||
clearTimeout(clickTimeout);
|
clickTimeout = setTimeout(() => {
|
||||||
clickTimeout = setTimeout(() => {
|
isClicking.value = false;
|
||||||
isClicking.value = false;
|
}, 100);
|
||||||
}, 100);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
function formatTime(seconds: number): string {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
@@ -228,7 +219,7 @@ function formatTime(seconds: number): string {
|
|||||||
|
|
||||||
function leaveGame() {
|
function leaveGame() {
|
||||||
colyseusService.leaveGame();
|
colyseusService.leaveGame();
|
||||||
router.push('/');
|
router.push(`/${routeUuid.value}`);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -83,12 +83,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||||
import PlayerStats from './games/PlayerStats.vue';
|
import PlayerStats from './games/PlayerStats.vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { colyseusService } from '../services/colyseus';
|
import { colyseusService } from '../services/colyseus';
|
||||||
import { getStateCallbacks } from 'colyseus.js';
|
import { getStateCallbacks } from 'colyseus.js';
|
||||||
import { localDB } from '../services/db';
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const routeUuid = computed(() => (route.params as any)?.uuid as string || '');
|
||||||
const inputName = ref('');
|
const inputName = ref('');
|
||||||
const isJoining = ref(false);
|
const isJoining = ref(false);
|
||||||
const colorInput = ref('#667eea');
|
const colorInput = ref('#667eea');
|
||||||
@@ -111,25 +112,9 @@ const previewPlayer = computed(() => ({
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
// Try reconnect to an ongoing game first
|
|
||||||
const reconnected = await colyseusService.tryReconnectToOngoingGame();
|
|
||||||
if (reconnected) {
|
|
||||||
router.push('/demo');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const room = await colyseusService.joinLobby();
|
const room = await colyseusService.joinLobby();
|
||||||
colorInput.value = colyseusService.playerColor.value || '#667eea';
|
colorInput.value = colyseusService.playerColor.value || '#667eea';
|
||||||
|
|
||||||
// Initialize local DB and prefill inputs if available
|
|
||||||
try {
|
|
||||||
await localDB.init();
|
|
||||||
const profile = localDB.getLocalPlayer();
|
|
||||||
if (profile?.name) inputName.value = profile.name;
|
|
||||||
if (profile?.color) colorInput.value = profile.color;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Local DB not available', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep color input synced with server-updated color
|
// Keep color input synced with server-updated color
|
||||||
watch(() => colyseusService.playerColor.value, (c) => {
|
watch(() => colyseusService.playerColor.value, (c) => {
|
||||||
if (c && c !== colorInput.value) colorInput.value = c;
|
if (c && c !== colorInput.value) colorInput.value = c;
|
||||||
@@ -190,7 +175,6 @@ onUnmounted(() => {
|
|||||||
async function updateName() {
|
async function updateName() {
|
||||||
// Send even if empty; server will assign a default unique name when empty
|
// Send even if empty; server will assign a default unique name when empty
|
||||||
const name = inputName.value.trim();
|
const name = inputName.value.trim();
|
||||||
try { localDB.setName(name); } catch {}
|
|
||||||
await colyseusService.setPlayerName(name);
|
await colyseusService.setPlayerName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +189,7 @@ async function handleQuickPlay() {
|
|||||||
console.log('Starting quickPlay...');
|
console.log('Starting quickPlay...');
|
||||||
try {
|
try {
|
||||||
const gameRoom = await colyseusService.quickPlay();
|
const gameRoom = await colyseusService.quickPlay();
|
||||||
console.log('Game room joined:', gameRoom?.id, 'Full room object:', gameRoom);
|
console.log('Game room joined. Full room object:', gameRoom);
|
||||||
|
|
||||||
// Leave the lobby room before navigating
|
// Leave the lobby room before navigating
|
||||||
if (colyseusService.lobbyRoom.value) {
|
if (colyseusService.lobbyRoom.value) {
|
||||||
@@ -214,8 +198,8 @@ async function handleQuickPlay() {
|
|||||||
colyseusService.lobbyRoom.value = null;
|
colyseusService.lobbyRoom.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Navigating to /demo...');
|
console.log('Navigating to /:uuid/demo...');
|
||||||
await router.push('/demo');
|
await router.push(`/${routeUuid.value}/demo`);
|
||||||
console.log('Navigation complete');
|
console.log('Navigation complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to join game:', error);
|
console.error('Failed to join game:', error);
|
||||||
@@ -234,7 +218,7 @@ async function joinRoom(roomId: string) {
|
|||||||
colyseusService.lobbyRoom.value.leave();
|
colyseusService.lobbyRoom.value.leave();
|
||||||
colyseusService.lobbyRoom.value = null;
|
colyseusService.lobbyRoom.value = null;
|
||||||
}
|
}
|
||||||
router.push('/demo');
|
router.push(`/${routeUuid.value}/demo`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to join room:', error);
|
console.error('Failed to join room:', error);
|
||||||
isJoining.value = false;
|
isJoining.value = false;
|
||||||
|
|||||||
@@ -6,15 +6,21 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 3004,
|
port: 3004,
|
||||||
allowedHosts: ['z590.interno.com'],
|
allowedHosts: ['z590.interno.com', 'snatchgame.interno.com'],
|
||||||
cors: {
|
cors: {
|
||||||
origin: ['http://localhost:3004', 'http://z590.interno.com:3004']
|
origin: ['http://localhost:3004', 'http://z590.interno.com:3004']
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3000',
|
target: process.env.VITE_DEV_API_PROXY_TARGET || 'http://localhost:3000',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/ws': {
|
||||||
|
target: process.env.VITE_DEV_WS_PROXY_TARGET || 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/ws/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"dev:client": "cd client && npm run dev",
|
"dev:client": "cd client && npm run dev",
|
||||||
"build": "npm run build:server && npm run build:client",
|
"build": "npm run build:server && npm run build:client",
|
||||||
"build:server": "cd server && npm run build",
|
"build:server": "cd server && npm run build",
|
||||||
"build:client": "cd client && npm run build"
|
"build:client": "cd client && npm run build",
|
||||||
|
"generate:uuids": "node scripts/generate-uuids.js --count 210"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
92
scripts/generate-uuids.js
Normal file
92
scripts/generate-uuids.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Generate UUIDs and write them to server/src/config/uuids.json.
|
||||||
|
// Usage:
|
||||||
|
// node scripts/generate-uuids.js --count 210 [--force|--append]
|
||||||
|
// Defaults: count=210, no overwrite if file has entries.
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = { count: 210, force: false, append: false };
|
||||||
|
for (let i = 2; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === "--force") args.force = true;
|
||||||
|
else if (a === "--append") args.append = true;
|
||||||
|
else if (a === "--count") {
|
||||||
|
const n = Number(argv[++i]);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) throw new Error("--count must be a positive number");
|
||||||
|
args.count = Math.floor(n);
|
||||||
|
} else if (/^--count=/.test(a)) {
|
||||||
|
const n = Number(a.split("=")[1]);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) throw new Error("--count must be a positive number");
|
||||||
|
args.count = Math.floor(n);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown argument: ${a}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genUuid() {
|
||||||
|
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
||||||
|
// Fallback: RFC4122 v4-ish
|
||||||
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
|
||||||
|
const r = (crypto.randomBytes(1)[0] % 16) | 0;
|
||||||
|
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueUuids(n, existing = new Set()) {
|
||||||
|
const set = new Set(existing);
|
||||||
|
while (set.size < n) set.add(genUuid());
|
||||||
|
return Array.from(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const { count, force, append } = parseArgs(process.argv);
|
||||||
|
const target = path.resolve(__dirname, "../server/src/config/uuids.json");
|
||||||
|
|
||||||
|
let current = [];
|
||||||
|
if (fs.existsSync(target)) {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(target, "utf8");
|
||||||
|
const arr = JSON.parse(raw);
|
||||||
|
if (Array.isArray(arr)) current = arr.filter(x => typeof x === "string");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && !append && current.length > 0) {
|
||||||
|
console.log(`uuids.json already has ${current.length} entries. Use --force to overwrite or --append to add.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = [];
|
||||||
|
if (force) {
|
||||||
|
out = uniqueUuids(count);
|
||||||
|
} else if (append) {
|
||||||
|
const needed = Math.max(count, current.length + count);
|
||||||
|
const set = new Set(current);
|
||||||
|
out = uniqueUuids(needed, set);
|
||||||
|
} else {
|
||||||
|
// initial write when empty
|
||||||
|
out = uniqueUuids(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||||
|
fs.writeFileSync(target, JSON.stringify(out, null, 2) + "\n", "utf8");
|
||||||
|
|
||||||
|
console.log(`Wrote ${out.length} UUIDs to ${path.relative(process.cwd(), target)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message || err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
212
server/src/config/uuids.json
Normal file
212
server/src/config/uuids.json
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
[
|
||||||
|
"8df02e8c-01b5-48be-a3e4-00eaf98324ba",
|
||||||
|
"295da55d-9273-4ac1-be95-cda7ad8a9437",
|
||||||
|
"62f2e8bc-e440-48ef-a909-c02ec1631bea",
|
||||||
|
"c76e9864-6c8e-4289-9e0c-5b72996b2246",
|
||||||
|
"fa430689-838c-4463-aa03-69aad1147c33",
|
||||||
|
"8bda0768-1a51-47de-903b-017726436cdb",
|
||||||
|
"1a1f8a8b-cd26-4ec6-a0c8-0041be19aa29",
|
||||||
|
"8d23a06b-0374-4df0-b04d-7a752731b88b",
|
||||||
|
"942368e1-3222-42e4-8843-a566820bdbe5",
|
||||||
|
"08177310-2120-4a92-9400-651b101e01f5",
|
||||||
|
"df82db78-fa3f-48ae-bc39-2baac6dfe136",
|
||||||
|
"713d6450-d7b5-4780-8a46-92b7e402ea6f",
|
||||||
|
"c437cd56-abc9-4018-9a6f-3a6154f1cefd",
|
||||||
|
"0d5c455c-d4e6-4fd1-a941-13e98ff01892",
|
||||||
|
"1512b77d-20c3-4297-a3c5-186f2ba07c60",
|
||||||
|
"fe094c84-53b8-4341-9d1d-faa56c11b5f4",
|
||||||
|
"9a1e97ac-d55a-491d-addf-278c339d948f",
|
||||||
|
"9cb42819-14a0-4e2c-900a-53d3077446df",
|
||||||
|
"69eddab2-0a7e-451c-b57c-e6d44f3233b0",
|
||||||
|
"4e87cb12-c7ee-4fec-8160-c1b137c5a543",
|
||||||
|
"fbdbcb18-edb4-4cb7-badd-9a143279db9d",
|
||||||
|
"efa9cda2-d493-4594-b43b-d27ecee55888",
|
||||||
|
"fccd77dd-80dc-4de6-ab22-3be4ae5249d2",
|
||||||
|
"107ce00b-8b40-4e6a-a43b-c6eba1e8b37d",
|
||||||
|
"517a172f-ce37-48ad-9c88-74d0892e4353",
|
||||||
|
"f15a682e-407a-48c3-986d-bf088ec3e771",
|
||||||
|
"5384b49e-c8a6-4d4f-a6b4-4a2af39d96db",
|
||||||
|
"e7e5597d-4795-4693-a837-d2ff5a1f0cc6",
|
||||||
|
"d9d59c56-f187-4de6-9a2b-3ea362cd963d",
|
||||||
|
"7dfb28b8-4eac-427c-a035-756927714c26",
|
||||||
|
"db679d7d-dd96-49cb-8d12-70bc9df3373c",
|
||||||
|
"98f91e36-9dfb-406c-9535-3c32aa247c66",
|
||||||
|
"d61c6569-e989-41aa-9751-6f560c983811",
|
||||||
|
"3fb1eca8-81fa-4d0d-92a8-908ce3df93a7",
|
||||||
|
"8b954ba6-22e7-4263-af82-7f2765b39cfc",
|
||||||
|
"0b4831c6-7d74-4871-b768-7b72ec09ada4",
|
||||||
|
"72da1235-919f-4f9a-8b5e-135398b0eede",
|
||||||
|
"9d716b0a-a890-4a07-85c1-54158edd817e",
|
||||||
|
"18540b84-ff5c-4cd7-8bb6-f6c56153da6e",
|
||||||
|
"73ec5ef7-33db-46e3-a3c0-7a7957869bd6",
|
||||||
|
"14b12ee6-1e00-4874-9538-cc8c87c2627d",
|
||||||
|
"2060d21a-522c-4982-ac0b-a8880a3d6cdf",
|
||||||
|
"6690d45e-e66a-466b-b974-1c310682c6fd",
|
||||||
|
"bf49a5e5-7a7a-4249-958d-8617d067c1c7",
|
||||||
|
"3a71e8ff-5cf9-4d4e-bbe6-96afa5274be4",
|
||||||
|
"a3548476-81e6-4443-a836-1720d7d05d6e",
|
||||||
|
"07b633bb-7abd-4748-a68d-87efde1c6c51",
|
||||||
|
"de405584-d2de-4b94-b263-d13f3757a95c",
|
||||||
|
"7615cd00-b8c9-4e8d-b521-cc4add933fa7",
|
||||||
|
"1a76009c-fd75-43b6-9f0a-c6b00f4456ab",
|
||||||
|
"c3e8977f-aed9-478e-92ad-c459e6e3a558",
|
||||||
|
"2898b6c8-553a-4756-ba70-9d867ff1a5fd",
|
||||||
|
"7e83f1af-55a5-46ae-98a8-96ea88dfebb9",
|
||||||
|
"b4c6bca6-65af-489b-8617-b1d85dba86e2",
|
||||||
|
"e47def64-e799-4883-af2d-c3611e41e76e",
|
||||||
|
"e18446ee-2ecd-45d6-bbf5-8afe3bcc15a3",
|
||||||
|
"bb86fc01-4d33-463d-9d2b-663371e52808",
|
||||||
|
"b3c3a8b7-d1f6-4ef6-a10d-bde179b35122",
|
||||||
|
"e0d32bc9-b8a6-4ed0-b153-c45ca1ece6be",
|
||||||
|
"d2f66ef8-54be-4ae3-ac8b-0e4a17140d84",
|
||||||
|
"d94ea1fa-21ba-4c81-ab61-479f604b76f6",
|
||||||
|
"a5ae508a-e8c4-4db3-b27d-e7be4b0f2d63",
|
||||||
|
"7b9aff5c-840f-4b35-85cd-0cbd00badc52",
|
||||||
|
"c67968f2-db78-41d9-bcd7-f87a8ac27d26",
|
||||||
|
"3b248115-658d-435c-8eaa-1b47dbe60a19",
|
||||||
|
"b4375a97-ced1-42de-91db-e3c2ed476149",
|
||||||
|
"69fb71a8-a95f-4462-9221-c68621f27f9f",
|
||||||
|
"767c6049-c892-46c3-955f-7931b6ff6973",
|
||||||
|
"0b9f828e-56ed-44c7-b995-7ebec6eaf9f2",
|
||||||
|
"6031717b-3433-475d-be83-1b52c005df44",
|
||||||
|
"51849e97-1212-4d2f-a308-f7b552be5a53",
|
||||||
|
"537c40dd-d06a-464e-bc21-f216524fb27f",
|
||||||
|
"339e5a3e-261b-40c4-bda9-0e49dce30f80",
|
||||||
|
"e3663772-b3d8-4b27-b01e-5b88ee43dc85",
|
||||||
|
"4280c9ba-fd6d-40e5-b6f3-968d338457c6",
|
||||||
|
"b0e16d48-d4e6-4129-b2f9-d070855dd107",
|
||||||
|
"b6e375cb-edac-4e43-be3c-b16d332162c3",
|
||||||
|
"bdefc162-9a36-4b74-a055-3b0146f3483c",
|
||||||
|
"3d422adf-1533-4f65-a35e-400bdc412295",
|
||||||
|
"73e0c5bb-8c2c-449e-a6c1-f3f5dbca1302",
|
||||||
|
"b59ec692-eead-4935-a0c0-aa38e562e0f4",
|
||||||
|
"4116f677-3386-47ce-aec5-72abb20bebef",
|
||||||
|
"e22229f5-dee5-4823-b0dd-7f7fa702b433",
|
||||||
|
"86ff368c-591e-42cd-b70f-d9019cde0277",
|
||||||
|
"23298dc5-64ea-4c14-9014-afe9577befff",
|
||||||
|
"ab596ec4-8016-4327-b61a-974ffd87d11c",
|
||||||
|
"0517b0c1-2546-4ec6-a224-c268622bdde5",
|
||||||
|
"2cd241b0-60cf-4413-a644-11d4a955842f",
|
||||||
|
"90a5b4c8-e86d-484c-8c63-47884c2ec96d",
|
||||||
|
"79e022ab-c068-456b-8045-d6a29d33e01d",
|
||||||
|
"39815855-fd67-414b-9ec7-cb65379984b5",
|
||||||
|
"aab80f37-f4e0-43f0-bf13-3627cf1b29ce",
|
||||||
|
"798de169-ba78-4ea6-85a7-19cf8061c76a",
|
||||||
|
"84d3cb6d-5f1f-4462-bafb-0ba7dc4ee31c",
|
||||||
|
"0fd349cb-979c-41a5-8f9d-d315905b8f58",
|
||||||
|
"694b7501-8b75-490f-bf47-e7fad08c8d05",
|
||||||
|
"a4384446-3a60-4fe7-9141-eee5a13cbc00",
|
||||||
|
"fa8337a3-fd52-49bb-a37b-429f131c98a1",
|
||||||
|
"6e1d1689-0d35-4dc2-a29b-cf7084f5d6b5",
|
||||||
|
"3fba6cf9-f0f4-40df-8102-66bf215730c6",
|
||||||
|
"72b553a7-d32d-4dc7-8d49-8a7a160142d1",
|
||||||
|
"049f9df1-e4c0-4d80-b7c7-9f4696dd915b",
|
||||||
|
"bfab4629-199d-4903-bcfb-e335fd0ae6a7",
|
||||||
|
"ff894277-9a98-43df-add4-349dc4929042",
|
||||||
|
"2933833d-98f2-41bd-838a-d80cf481142b",
|
||||||
|
"7cedb724-c4d0-4b70-b9a7-7f5b6dfe8521",
|
||||||
|
"1adbece5-e0e8-43a2-89bd-0b2d0883f435",
|
||||||
|
"9cf256fd-a6c9-4089-ab2a-8f6539197996",
|
||||||
|
"02511ad5-2503-4091-8606-79155bb79786",
|
||||||
|
"570c966a-882f-498e-b988-8408c74d16ac",
|
||||||
|
"ddbf38df-398b-4633-8d33-9acdc61b0150",
|
||||||
|
"95532a62-2ae3-47c0-af26-0e52d531a5c8",
|
||||||
|
"20aee4d3-75ee-484b-bc92-65c682981ebe",
|
||||||
|
"7395a1fe-2ab3-432f-a888-bcc9daaa08ef",
|
||||||
|
"d660de2c-57d9-4366-a149-94810abecb82",
|
||||||
|
"07dda1ba-dc8a-4fe3-9188-3d644a1992b4",
|
||||||
|
"e2cd81bb-1511-47a5-b560-f0c282f30bda",
|
||||||
|
"42051ccf-bd08-4e43-a32d-c96c9649e329",
|
||||||
|
"50fda467-da61-4cef-8d86-650eacdeaaca",
|
||||||
|
"89f33d75-90d6-43af-9fd4-e1e481c274ff",
|
||||||
|
"8da6c0ad-447e-470d-8f98-891e518f942b",
|
||||||
|
"421b4858-137a-4935-816d-3ee5f856f007",
|
||||||
|
"47465c1c-5955-48ea-ba46-64a323ba7f70",
|
||||||
|
"54d8d4c6-3c18-4d19-877b-d41b632fc81e",
|
||||||
|
"cb93a50f-5585-4d84-9405-85598bb30deb",
|
||||||
|
"831252bd-8e18-4471-a4d5-01c84256159e",
|
||||||
|
"81f68488-7cff-44ab-ae08-73c51aac44a3",
|
||||||
|
"55ed1c19-7648-4a17-83da-507ed81798e3",
|
||||||
|
"2d3e55e5-991f-4122-a0fa-260402c7a5b5",
|
||||||
|
"3795b64a-eb15-4bc9-a7b2-9d8f9e0d2708",
|
||||||
|
"390f0af1-81a6-46c1-b2fc-70c5ec2cba37",
|
||||||
|
"9d026d2b-5a58-45d2-bf74-a79fb9b733e9",
|
||||||
|
"7f16bc71-25e9-4986-8e77-8e6f7bc2c1e3",
|
||||||
|
"2e3ea8e0-c776-481b-ac55-df6aa628772a",
|
||||||
|
"1ad99851-acea-4d20-b8d3-b891b24c949d",
|
||||||
|
"9dd15f53-cc4c-4a39-9a4c-33d37152b487",
|
||||||
|
"85f42124-c6d5-4cc2-93c6-6530d2c0a262",
|
||||||
|
"125a9ff8-71f7-432c-b368-d488c05bbea4",
|
||||||
|
"18064098-20c0-4bc6-9296-221dbe0cd91b",
|
||||||
|
"7506497e-975f-4bd8-a8cf-26b33de53936",
|
||||||
|
"b87f44f6-26ba-4251-9541-9c3e6d520f6e",
|
||||||
|
"b344d20f-738a-4361-ac64-752dd859ecf4",
|
||||||
|
"c836291f-c1eb-4d7e-8f0f-6f19113b0624",
|
||||||
|
"5e1dcd2c-3e8e-4970-b336-475fd10d9723",
|
||||||
|
"116f07da-dc77-48fe-a49b-8d904cad34b1",
|
||||||
|
"830086b5-89d7-4740-b99a-bfd35a4256df",
|
||||||
|
"b7a375f6-a2fd-415e-bb7d-545208e211ed",
|
||||||
|
"a129bf82-a975-4d52-97b6-635873217883",
|
||||||
|
"6f6c5a5d-402d-4b62-b70c-1877c43d21d3",
|
||||||
|
"f2760c22-0b06-4a31-bf9e-e07a39d23396",
|
||||||
|
"50b6b35e-1d63-4e00-9e85-9974efecb3af",
|
||||||
|
"8307716d-b4e2-4424-95b2-cac3c3fe8fbd",
|
||||||
|
"f7f023b2-a557-435d-9749-82ff3cae7aaf",
|
||||||
|
"b80b1799-c1b2-475a-94e2-fa7704dc570b",
|
||||||
|
"7914a266-ec02-4ecf-b789-0ca7ddf5b5fd",
|
||||||
|
"e8bfaf2e-2ca8-4efc-917e-d9b06a793fd0",
|
||||||
|
"4408781d-951b-4f0b-b33b-08bdc2f1ebdf",
|
||||||
|
"8cfb3846-d177-4273-a5dc-1687a9651059",
|
||||||
|
"52326d11-1f13-4642-9536-ec72e779742c",
|
||||||
|
"a1f39982-1353-4ad6-a7e8-5f52d1433bca",
|
||||||
|
"512ffbf1-581c-4ab5-a062-acee5a27ab8b",
|
||||||
|
"dac86456-9028-4205-b589-069b25444675",
|
||||||
|
"8a2e57fe-5fb3-4322-a34d-053b5a57f542",
|
||||||
|
"a57972b6-0e86-4294-925c-30e1316693aa",
|
||||||
|
"9e36b551-0956-4861-80c4-72bef2e4d788",
|
||||||
|
"375d5b6e-8690-4fce-aa0b-61f146fcada4",
|
||||||
|
"c9a48043-502a-4872-8302-43408cc1629c",
|
||||||
|
"d59b6da9-a887-487d-96c2-c0f3e48d6faa",
|
||||||
|
"f928ae74-7187-4025-8d97-083d22741383",
|
||||||
|
"0b094fc1-fcea-4bc6-a980-379621ea8717",
|
||||||
|
"610a1eff-b097-499e-9861-4fc06a17d60e",
|
||||||
|
"eb1a6f6e-65b7-4b8d-ad41-76edc64188aa",
|
||||||
|
"75da10be-c838-4b45-9bba-df745b34d62f",
|
||||||
|
"fb158cb8-bd69-4378-9a7c-6aeb9efa7bed",
|
||||||
|
"3d64be5f-2e9b-47f5-a084-b52680766b6f",
|
||||||
|
"fee4f458-c4d9-4dd5-902b-892862a70156",
|
||||||
|
"241671bb-45f6-4e88-8bd1-3910bbd8b9b3",
|
||||||
|
"13a3af48-d534-420f-82ed-7a5c57470789",
|
||||||
|
"b1fc197e-b583-4226-88aa-19e09b5c9412",
|
||||||
|
"8b4ff431-e3e6-4446-8a31-227c7e4ac5f9",
|
||||||
|
"780fcf48-9b8d-47c6-97f7-5bbf069e9631",
|
||||||
|
"ae780028-e98e-4c78-bead-3e31f50c5d50",
|
||||||
|
"5e440039-f316-443a-a0df-782a7766d8e1",
|
||||||
|
"9a9db992-0417-4c4f-a544-d96c6d529134",
|
||||||
|
"1fa1499f-1e5e-4439-be7e-bd72778d0df7",
|
||||||
|
"93b12ad8-8313-4c67-ac51-be35dfbd6112",
|
||||||
|
"85503d1c-3fc3-4dac-a4b5-9b4f2d8eb851",
|
||||||
|
"b44ee060-4eda-4a5e-8c1e-50b820152b7d",
|
||||||
|
"df73713b-7f80-4790-95c2-74002dc0d59a",
|
||||||
|
"c012d355-b501-4563-84a5-089e5e9acb10",
|
||||||
|
"5d1d9afc-0d77-416f-a06d-1a8c29014bf7",
|
||||||
|
"9312e135-6aea-49f8-8734-092696641aab",
|
||||||
|
"6573c846-9974-4a44-99ff-db5b002c43d9",
|
||||||
|
"fd7c1b34-a6dd-4b99-adb5-3ad4e1c50e4c",
|
||||||
|
"4d5f7d8d-3b17-4eee-b4fa-b08f07509d34",
|
||||||
|
"b0d05423-9cbd-4904-862b-3c8f8b47e6e7",
|
||||||
|
"c7909ee2-6334-4965-b107-b82c3116552b",
|
||||||
|
"fe52489b-bc66-406f-b2be-2635b35002ee",
|
||||||
|
"b2eb5dcf-b7c8-4e50-ab3d-49b5a05319d2",
|
||||||
|
"e40e6b77-d032-44d9-943a-bf6fe1a5e152",
|
||||||
|
"82a3c8ee-94ef-4a16-bb1b-5ee6aa20a23c",
|
||||||
|
"3a8a1962-f1b4-44a2-8532-ea17dfbf6724",
|
||||||
|
"60c07203-dc60-47df-a1c5-fd6761da7a8d",
|
||||||
|
"776936b8-7f3f-4357-a73e-11cd3367d681",
|
||||||
|
"9cfa7d17-f595-4d2d-90ef-287337d6eaa8",
|
||||||
|
"7743cd91-bf64-4ba7-9188-5343adfa8268",
|
||||||
|
"f0bde278-86aa-41f3-8a6a-fc2cc83e9cfb",
|
||||||
|
"6209690c-7f04-42fb-b2ae-e266c0589eb9",
|
||||||
|
"21cea9b2-78ed-4da2-a277-b266643e92be",
|
||||||
|
"92e0c4d0-b98e-4238-b80a-436b12dec827"
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Room, Client, matchMaker } from "colyseus";
|
import { Room, Client, matchMaker } from "colyseus";
|
||||||
import { LobbyState, AvailableRoom } from "./schemas/LobbyState";
|
import { LobbyState, AvailableRoom } from "./schemas/LobbyState";
|
||||||
import { NameManager } from "../utils/nameManager";
|
import { NameManager } from "../utils/nameManager";
|
||||||
|
import { isUuidAllowed } from "../utils/uuidRegistry";
|
||||||
|
|
||||||
export class LobbyRoom extends Room<LobbyState> {
|
export class LobbyRoom extends Room<LobbyState> {
|
||||||
private updateInterval?: NodeJS.Timeout;
|
private updateInterval?: NodeJS.Timeout;
|
||||||
@@ -33,6 +34,12 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
|
|
||||||
onJoin(client: Client, options: any) {
|
onJoin(client: Client, options: any) {
|
||||||
console.log(`[LobbyRoom] ${client.sessionId} joined lobby with UUID: ${options.uuid}`);
|
console.log(`[LobbyRoom] ${client.sessionId} joined lobby with UUID: ${options.uuid}`);
|
||||||
|
// Enforce UUID presence and allowlist (if configured)
|
||||||
|
if (!options.uuid || !isUuidAllowed(options.uuid)) {
|
||||||
|
try { client.send("error", { message: "UUID inválido o faltante" }); } catch {}
|
||||||
|
try { client.leave(1000); } catch {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Store UUID mapping if provided
|
// Store UUID mapping if provided
|
||||||
if (options.uuid) {
|
if (options.uuid) {
|
||||||
@@ -47,6 +54,11 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
// Add player temporarily to lobby state
|
// Add player temporarily to lobby state
|
||||||
const existingName = NameManager.getInstance().getPlayerName(options.uuid);
|
const existingName = NameManager.getInstance().getPlayerName(options.uuid);
|
||||||
this.state.addPlayer(client.sessionId, existingName || "");
|
this.state.addPlayer(client.sessionId, existingName || "");
|
||||||
|
const existingColor = NameManager.getInstance().getPlayerColor(options.uuid);
|
||||||
|
if (existingColor) {
|
||||||
|
const p = this.state.players.get(client.sessionId);
|
||||||
|
if (p) p.color = existingColor;
|
||||||
|
}
|
||||||
|
|
||||||
// Send welcome first
|
// Send welcome first
|
||||||
client.send("welcome", {
|
client.send("welcome", {
|
||||||
@@ -68,19 +80,17 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
// Check if this UUID already has a name
|
// Check if this UUID already has a name
|
||||||
const existingName = NameManager.getInstance().getPlayerName(options.uuid);
|
const existingName = NameManager.getInstance().getPlayerName(options.uuid);
|
||||||
this.state.addPlayer(client.sessionId, existingName || "");
|
this.state.addPlayer(client.sessionId, existingName || "");
|
||||||
|
const existingColor = NameManager.getInstance().getPlayerColor(options.uuid);
|
||||||
|
if (existingColor) {
|
||||||
|
const p = this.state.players.get(client.sessionId);
|
||||||
|
if (p) p.color = existingColor;
|
||||||
|
}
|
||||||
|
|
||||||
client.send("welcome", {
|
client.send("welcome", {
|
||||||
sessionId: client.sessionId,
|
sessionId: client.sessionId,
|
||||||
name: existingName || "",
|
name: existingName || "",
|
||||||
color: this.state.players.get(client.sessionId)?.color || "#667eea"
|
color: this.state.players.get(client.sessionId)?.color || "#667eea"
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Fallback for clients without UUID (shouldn't happen in normal flow)
|
|
||||||
this.state.addPlayer(client.sessionId, "");
|
|
||||||
client.send("welcome", {
|
|
||||||
sessionId: client.sessionId,
|
|
||||||
color: this.state.players.get(client.sessionId)?.color || "#667eea"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateAvailableRooms();
|
this.updateAvailableRooms();
|
||||||
@@ -116,7 +126,7 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uuid = this.sessionToUuid.get(client.sessionId) || client.sessionId;
|
const uuid = this.sessionToUuid.get(client.sessionId) || client.sessionId;
|
||||||
const uniqueName = NameManager.getInstance().generateUniquePlayerName(data.name, uuid);
|
const uniqueName = NameManager.getInstance().setPlayerName(uuid, data.name);
|
||||||
|
|
||||||
currentPlayer.name = uniqueName;
|
currentPlayer.name = uniqueName;
|
||||||
|
|
||||||
@@ -134,6 +144,8 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentPlayer.color = sanitized;
|
currentPlayer.color = sanitized;
|
||||||
|
const uuid = this.sessionToUuid.get(client.sessionId) || client.sessionId;
|
||||||
|
NameManager.getInstance().setPlayerColor(uuid, sanitized);
|
||||||
client.send("colorUpdated", { color: sanitized });
|
client.send("colorUpdated", { color: sanitized });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,4 +245,4 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
console.error("[LobbyRoom] Error updating available rooms:", error);
|
console.error("[LobbyRoom] Error updating available rooms:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export class NameManager {
|
export class NameManager {
|
||||||
private static instance: NameManager;
|
private static instance: NameManager;
|
||||||
private uuidToName: Map<string, string> = new Map();
|
private uuidToName: Map<string, string> = new Map();
|
||||||
|
private uuidToColor: Map<string, string> = new Map();
|
||||||
|
|
||||||
// For shuffle functionality
|
// For shuffle functionality
|
||||||
private roomAssignments: Map<string, { roomId: string; role: 'P1' | 'P2' }> = new Map();
|
private roomAssignments: Map<string, { roomId: string; role: 'P1' | 'P2' }> = new Map();
|
||||||
@@ -15,31 +16,27 @@ export class NameManager {
|
|||||||
return NameManager.instance;
|
return NameManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy method used for first-time assignment; keeps global uniqueness.
|
||||||
generateUniquePlayerName(baseName: string, uuid: string): string {
|
generateUniquePlayerName(baseName: string, uuid: string): string {
|
||||||
// If UUID already has a name, return it
|
const normalizedName = (baseName || '').trim().toLowerCase() || 'guest';
|
||||||
const existingName = this.uuidToName.get(uuid);
|
return this.setPlayerName(uuid, normalizedName);
|
||||||
if (existingName) {
|
}
|
||||||
return existingName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedName = baseName.trim().toLowerCase();
|
// Explicitly set/update the name for a UUID, ensuring uniqueness across other UUIDs.
|
||||||
if (!normalizedName) {
|
setPlayerName(uuid: string, baseName: string): string {
|
||||||
// Default base name when none is provided
|
const normalizedName = (baseName || '').trim().toLowerCase() || 'guest';
|
||||||
return this.generateUniquePlayerName('guest', uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try exact name if not in use; otherwise, append incremental suffixes
|
const isInUseByOther = (name: string) => {
|
||||||
const isInUse = (name: string) => {
|
for (const [k, v] of this.uuidToName.entries()) {
|
||||||
for (const val of this.uuidToName.values()) {
|
if (k !== uuid && v === name) return true;
|
||||||
if (val === name) return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let uniqueName = normalizedName;
|
let uniqueName = normalizedName;
|
||||||
if (isInUse(uniqueName)) {
|
if (isInUseByOther(uniqueName)) {
|
||||||
let n = 2;
|
let n = 2;
|
||||||
while (isInUse(`${normalizedName}-${n}`)) n++;
|
while (isInUseByOther(`${normalizedName}-${n}`)) n++;
|
||||||
uniqueName = `${normalizedName}-${n}`;
|
uniqueName = `${normalizedName}-${n}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +53,17 @@ export class NameManager {
|
|||||||
return this.uuidToName.get(uuid);
|
return this.uuidToName.get(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color persistence per UUID
|
||||||
|
setPlayerColor(uuid: string, color: string): void {
|
||||||
|
const sanitized = (color || '').toString().trim();
|
||||||
|
if (!sanitized) return;
|
||||||
|
this.uuidToColor.set(uuid, sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerColor(uuid: string): string | undefined {
|
||||||
|
return this.uuidToColor.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
getAllActivePlayers(): string[] {
|
getAllActivePlayers(): string[] {
|
||||||
return Array.from(this.uuidToName.values());
|
return Array.from(this.uuidToName.values());
|
||||||
}
|
}
|
||||||
@@ -90,4 +98,4 @@ export class NameManager {
|
|||||||
getAllRoomAssignments(): Map<string, { roomId: string; role: 'P1' | 'P2' }> {
|
getAllRoomAssignments(): Map<string, { roomId: string; role: 'P1' | 'P2' }> {
|
||||||
return new Map(this.roomAssignments);
|
return new Map(this.roomAssignments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
server/src/utils/uuidRegistry.ts
Normal file
64
server/src/utils/uuidRegistry.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// UUID allowlist registry.
|
||||||
|
// Sources:
|
||||||
|
// - server/src/config/uuids.json (array of strings)
|
||||||
|
// - ENV `UUID_ALLOWLIST` (comma-separated)
|
||||||
|
// If both are provided, they are merged.
|
||||||
|
// If the final set is empty, any UUID is accepted (dev-friendly).
|
||||||
|
|
||||||
|
import uuidsFile from "../config/uuids.json";
|
||||||
|
|
||||||
|
let allowedUuids = new Set<string>();
|
||||||
|
|
||||||
|
function normalizeUuid(u: string): string | null {
|
||||||
|
const s = (u || "").trim();
|
||||||
|
if (!s) return null;
|
||||||
|
// Basic UUID v4 format guard; relax if your UUIDs differ
|
||||||
|
const re = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
||||||
|
return re.test(s) ? s.toLowerCase() : s.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromFile(): string[] {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(uuidsFile)) {
|
||||||
|
return uuidsFile.filter(x => typeof x === "string") as string[];
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromEnv(): string[] {
|
||||||
|
try {
|
||||||
|
const raw = process.env.UUID_ALLOWLIST || "";
|
||||||
|
if (!raw.trim()) return [];
|
||||||
|
return raw.split(",").map(s => s.trim()).filter(Boolean);
|
||||||
|
} catch {}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAllowlist(): Set<string> {
|
||||||
|
const set = new Set<string>();
|
||||||
|
const union = [...loadFromFile(), ...loadFromEnv()];
|
||||||
|
union.forEach(u => {
|
||||||
|
const n = normalizeUuid(u);
|
||||||
|
if (n) set.add(n);
|
||||||
|
});
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reloadUuidAllowlist(): void {
|
||||||
|
allowedUuids = buildAllowlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize on module load
|
||||||
|
reloadUuidAllowlist();
|
||||||
|
|
||||||
|
export function isUuidAllowed(uuid: string | undefined | null): boolean {
|
||||||
|
if (!uuid) return false;
|
||||||
|
if (allowedUuids.size === 0) return true;
|
||||||
|
const n = normalizeUuid(uuid);
|
||||||
|
return !!(n && allowedUuids.has(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllowedUuidCount(): number {
|
||||||
|
return allowedUuids.size;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user