nombre persitente por navegador
This commit is contained in:
7
client/package-lock.json
generated
7
client/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colyseus.js": "latest",
|
"colyseus.js": "latest",
|
||||||
|
"lokijs": "^1.5.12",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest"
|
"vue-router": "latest"
|
||||||
},
|
},
|
||||||
@@ -1270,6 +1271,12 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lokijs": {
|
||||||
|
"version": "1.5.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/lokijs/-/lokijs-1.5.12.tgz",
|
||||||
|
"integrity": "sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
"colyseus.js": "latest"
|
"colyseus.js": "latest",
|
||||||
|
"lokijs": "^1.5.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "latest",
|
"@vitejs/plugin-vue": "latest",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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;
|
||||||
@@ -24,12 +25,18 @@ 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 desiredName: string | null = null;
|
||||||
|
private nameRetryCount: number = 0;
|
||||||
|
private nameRetryTimer: any = null;
|
||||||
|
private readonly LS_KEY_NAME = "snatch.player.name";
|
||||||
|
private readonly LS_KEY_COLOR = "snatch.player.color";
|
||||||
|
|
||||||
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("");
|
||||||
public sessionId: Ref<string> = ref("");
|
public sessionId: Ref<string> = ref("");
|
||||||
public playerColor: Ref<string> = ref("#667eea");
|
public playerColor: Ref<string> = ref("#667eea");
|
||||||
|
public nameConfirmed: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const defaultHost = typeof window !== "undefined" ? window.location.hostname : "localhost";
|
const defaultHost = typeof window !== "undefined" ? window.location.hostname : "localhost";
|
||||||
@@ -42,6 +49,13 @@ class ColyseusService {
|
|||||||
const httpProtocol = typeof window !== "undefined" && window.location.protocol === "https:" ? "https" : "http";
|
const httpProtocol = typeof window !== "undefined" && window.location.protocol === "https:" ? "https" : "http";
|
||||||
const apiFallback = `${httpProtocol}://${defaultHost}:${defaultPort}/api`;
|
const apiFallback = `${httpProtocol}://${defaultHost}:${defaultPort}/api`;
|
||||||
this.apiBase = (import.meta.env as any).VITE_API_URL || apiFallback;
|
this.apiBase = (import.meta.env as any).VITE_API_URL || apiFallback;
|
||||||
|
// Hydrate from localStorage for immediate UI
|
||||||
|
try {
|
||||||
|
const savedName = typeof window !== 'undefined' ? (window.localStorage.getItem(this.LS_KEY_NAME) || "") : "";
|
||||||
|
const savedColor = typeof window !== 'undefined' ? (window.localStorage.getItem(this.LS_KEY_COLOR) || "") : "";
|
||||||
|
if (savedName) this.playerName.value = savedName;
|
||||||
|
if (savedColor) this.playerColor.value = savedColor;
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinLobby(): Promise<Room> {
|
async joinLobby(): Promise<Room> {
|
||||||
@@ -49,19 +63,77 @@ class ColyseusService {
|
|||||||
const room = await this.client.joinOrCreate("lobby");
|
const room = await this.client.joinOrCreate("lobby");
|
||||||
this.lobbyRoom.value = room;
|
this.lobbyRoom.value = room;
|
||||||
this.currentRoom = room;
|
this.currentRoom = room;
|
||||||
|
// Require explicit confirmation each time we join the lobby
|
||||||
|
this.nameConfirmed.value = false;
|
||||||
|
// Clear any pending name retry from previous sessions
|
||||||
|
this.desiredName = null;
|
||||||
|
this.nameRetryCount = 0;
|
||||||
|
if (this.nameRetryTimer) { clearTimeout(this.nameRetryTimer); this.nameRetryTimer = null; }
|
||||||
|
|
||||||
room.onMessage("welcome", (data) => {
|
room.onMessage("welcome", async (data) => {
|
||||||
this.sessionId.value = data.sessionId;
|
this.sessionId.value = data.sessionId;
|
||||||
this.playerName.value = data.assignedName;
|
|
||||||
if (data.color) this.playerColor.value = data.color;
|
if (data.color) this.playerColor.value = data.color;
|
||||||
|
// Initialize local DB and optionally auto-apply saved profile
|
||||||
|
try {
|
||||||
|
await localDB.init();
|
||||||
|
const profile = localDB.getLocalPlayer();
|
||||||
|
// Apply saved color silently
|
||||||
|
if (profile?.color && profile.color !== this.playerColor.value) {
|
||||||
|
this.setPlayerColor(profile.color);
|
||||||
|
}
|
||||||
|
let candidateName = profile?.name || "";
|
||||||
|
if (!candidateName) {
|
||||||
|
try { candidateName = typeof window !== 'undefined' ? (window.localStorage.getItem(this.LS_KEY_NAME) || "") : ""; } catch {}
|
||||||
|
}
|
||||||
|
if (candidateName) {
|
||||||
|
this.playerName.value = candidateName;
|
||||||
|
try { localDB.setName(candidateName); } catch {}
|
||||||
|
this.claimSavedName(candidateName);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Local DB init failed", e);
|
||||||
|
// Fallback purely to localStorage
|
||||||
|
try {
|
||||||
|
const candidateName = typeof window !== 'undefined' ? (window.localStorage.getItem(this.LS_KEY_NAME) || "") : "";
|
||||||
|
if (candidateName) {
|
||||||
|
this.playerName.value = candidateName;
|
||||||
|
this.claimSavedName(candidateName);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("nameUpdated", (data) => {
|
room.onMessage("nameUpdated", (data) => {
|
||||||
this.playerName.value = data.name;
|
this.playerName.value = data.name;
|
||||||
|
try { localDB.setName(data.name); } catch {}
|
||||||
|
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_NAME, data.name || ""); } catch {}
|
||||||
|
this.nameConfirmed.value = true;
|
||||||
|
|
||||||
|
// If we are trying to reclaim a saved name and got a suffixed one, retry briefly.
|
||||||
|
if (this.desiredName) {
|
||||||
|
const desired = (this.desiredName || "").trim().toLowerCase();
|
||||||
|
const got = (data.name || "").trim().toLowerCase();
|
||||||
|
if (desired && desired !== got && this.nameRetryCount < 2) {
|
||||||
|
const attempt = ++this.nameRetryCount;
|
||||||
|
if (this.nameRetryTimer) clearTimeout(this.nameRetryTimer);
|
||||||
|
this.nameRetryTimer = setTimeout(() => {
|
||||||
|
// Attempt again; if the previous session has now left, exact name may be free
|
||||||
|
this.setPlayerName(this.desiredName || "");
|
||||||
|
}, attempt * 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Either matched desired or exceeded retries; stop retrying
|
||||||
|
this.desiredName = null;
|
||||||
|
this.nameRetryCount = 0;
|
||||||
|
if (this.nameRetryTimer) { clearTimeout(this.nameRetryTimer); this.nameRetryTimer = null; }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("colorUpdated", (data) => {
|
room.onMessage("colorUpdated", (data) => {
|
||||||
if (data?.color) this.playerColor.value = data.color;
|
if (data?.color) {
|
||||||
|
this.playerColor.value = data.color;
|
||||||
|
try { localDB.setColor(data.color); } catch {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return room;
|
return room;
|
||||||
@@ -74,12 +146,26 @@ class ColyseusService {
|
|||||||
async setPlayerName(name: string): Promise<void> {
|
async setPlayerName(name: string): Promise<void> {
|
||||||
if (this.lobbyRoom.value) {
|
if (this.lobbyRoom.value) {
|
||||||
this.lobbyRoom.value.send("setName", name);
|
this.lobbyRoom.value.send("setName", name);
|
||||||
|
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_NAME, name || ""); } catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempt to reclaim the saved name with a short retry window to avoid race
|
||||||
|
private claimSavedName(name: string): void {
|
||||||
|
this.desiredName = name;
|
||||||
|
this.nameRetryCount = 0;
|
||||||
|
if (this.nameRetryTimer) { clearTimeout(this.nameRetryTimer); this.nameRetryTimer = null; }
|
||||||
|
// Small delay to let any previous session fully leave the lobby
|
||||||
|
this.nameRetryTimer = setTimeout(() => {
|
||||||
|
this.setPlayerName(name);
|
||||||
|
this.nameConfirmed.value = true;
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
async setPlayerColor(color: string): Promise<void> {
|
async setPlayerColor(color: string): Promise<void> {
|
||||||
if (this.lobbyRoom.value) {
|
if (this.lobbyRoom.value) {
|
||||||
this.lobbyRoom.value.send("setColor", color);
|
this.lobbyRoom.value.send("setColor", color);
|
||||||
|
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_COLOR, color || ""); } catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
90
client/src/services/db.ts
Normal file
90
client/src/services/db.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import Loki from "lokijs";
|
||||||
|
|
||||||
|
export interface LocalPlayerDoc {
|
||||||
|
id: string; // fixed id for local profile
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
stats: {
|
||||||
|
totalClicks: number;
|
||||||
|
gamesPlayed: number;
|
||||||
|
wins: number;
|
||||||
|
losses: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalDBService {
|
||||||
|
private db: Loki | null = null;
|
||||||
|
private players: Collection<LocalPlayerDoc> | null = null;
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
if (this.initialized) return;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// In browser, Loki uses localStorage adapter by default; no need to pass one.
|
||||||
|
this.db = new Loki("snatchgame.local.db", {
|
||||||
|
autoload: true,
|
||||||
|
autoloadCallback: () => {
|
||||||
|
this.players = this.db!.getCollection<LocalPlayerDoc>("players");
|
||||||
|
if (!this.players) {
|
||||||
|
this.players = this.db!.addCollection<LocalPlayerDoc>("players", { unique: ["id"] });
|
||||||
|
}
|
||||||
|
// Ensure local profile exists
|
||||||
|
if (!this.players.by("id", "local")) {
|
||||||
|
this.players.insert({
|
||||||
|
id: "local",
|
||||||
|
name: "",
|
||||||
|
color: "#667eea",
|
||||||
|
stats: { totalClicks: 0, gamesPlayed: 0, wins: 0, losses: 0 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
this.db!.saveDatabase(() => resolve());
|
||||||
|
},
|
||||||
|
autosave: true,
|
||||||
|
autosaveInterval: 2000
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private get col(): Collection<LocalPlayerDoc> {
|
||||||
|
if (!this.players) throw new Error("LocalDB not initialized");
|
||||||
|
return this.players;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalPlayer(): LocalPlayerDoc {
|
||||||
|
const doc = this.col.by("id", "local");
|
||||||
|
return doc as LocalPlayerDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name: string): void {
|
||||||
|
const doc = this.getLocalPlayer();
|
||||||
|
doc.name = name;
|
||||||
|
this.col.update(doc);
|
||||||
|
this.db?.saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
setColor(color: string): void {
|
||||||
|
const doc = this.getLocalPlayer();
|
||||||
|
doc.color = color;
|
||||||
|
this.col.update(doc);
|
||||||
|
this.db?.saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
incClicks(delta = 1): void {
|
||||||
|
const doc = this.getLocalPlayer();
|
||||||
|
doc.stats.totalClicks = (doc.stats.totalClicks || 0) + delta;
|
||||||
|
this.col.update(doc);
|
||||||
|
this.db?.saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
recordGame(result: "win" | "loss"): void {
|
||||||
|
const doc = this.getLocalPlayer();
|
||||||
|
doc.stats.gamesPlayed = (doc.stats.gamesPlayed || 0) + 1;
|
||||||
|
if (result === "win") doc.stats.wins = (doc.stats.wins || 0) + 1;
|
||||||
|
else doc.stats.losses = (doc.stats.losses || 0) + 1;
|
||||||
|
this.col.update(doc);
|
||||||
|
this.db?.saveDatabase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const localDB = new LocalDBService();
|
||||||
@@ -77,6 +77,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } 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();
|
||||||
@@ -197,6 +198,7 @@ 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);
|
||||||
|
|||||||
@@ -29,10 +29,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-actions">
|
<div class="main-actions">
|
||||||
<button @click="handleQuickPlay" class="btn btn-primary btn-large" :disabled="isJoining">
|
<button @click="handleQuickPlay" class="btn btn-primary btn-large" :disabled="isJoining || !nameConfirmed">
|
||||||
<span v-if="!isJoining">🧪 Demo Play</span>
|
<span v-if="!isJoining">🧪 Demo Play</span>
|
||||||
<span v-else>Finding match...</span>
|
<span v-else>Finding match...</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div v-if="!nameConfirmed" class="hint">Antes de jugar, presiona "Set Name" para confirmar tu nombre.</div>
|
||||||
|
<div v-else class="hint ok">Nombre confirmado ✔</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rooms-section">
|
<div class="rooms-section">
|
||||||
@@ -45,7 +47,8 @@
|
|||||||
v-for="room in availableRooms"
|
v-for="room in availableRooms"
|
||||||
:key="room.roomId"
|
:key="room.roomId"
|
||||||
class="room-card"
|
class="room-card"
|
||||||
@click="joinRoom(room.roomId)"
|
:class="{ disabled: !nameConfirmed }"
|
||||||
|
@click="nameConfirmed ? joinRoom(room.roomId) : null"
|
||||||
>
|
>
|
||||||
<div class="room-info">
|
<div class="room-info">
|
||||||
<span class="room-id">Room #{{ room.roomId.slice(0, 6) }}</span>
|
<span class="room-id">Room #{{ room.roomId.slice(0, 6) }}</span>
|
||||||
@@ -78,11 +81,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } 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 } 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 inputName = ref('');
|
const inputName = ref('');
|
||||||
@@ -93,6 +97,7 @@ const onlinePlayers = ref<any[]>([]);
|
|||||||
const totalPlayers = ref(0);
|
const totalPlayers = ref(0);
|
||||||
|
|
||||||
const playerName = computed(() => colyseusService.playerName.value);
|
const playerName = computed(() => colyseusService.playerName.value);
|
||||||
|
const nameConfirmed = computed(() => colyseusService.nameConfirmed.value);
|
||||||
const playerColor = computed(() => colyseusService.playerColor.value);
|
const playerColor = computed(() => colyseusService.playerColor.value);
|
||||||
const previewPlayer = computed(() => ({
|
const previewPlayer = computed(() => ({
|
||||||
sessionId: 'preview',
|
sessionId: 'preview',
|
||||||
@@ -108,6 +113,21 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
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
|
||||||
|
watch(() => colyseusService.playerColor.value, (c) => {
|
||||||
|
if (c && c !== colorInput.value) colorInput.value = c;
|
||||||
|
});
|
||||||
const $ = getStateCallbacks(room);
|
const $ = getStateCallbacks(room);
|
||||||
|
|
||||||
$(room.state).listen("availableRooms", (value: any) => {
|
$(room.state).listen("availableRooms", (value: any) => {
|
||||||
@@ -162,10 +182,10 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function updateName() {
|
async function updateName() {
|
||||||
if (inputName.value.trim()) {
|
// Send even if empty; server will assign a default unique name when empty
|
||||||
await colyseusService.setPlayerName(inputName.value.trim());
|
const name = inputName.value.trim();
|
||||||
inputName.value = '';
|
try { localDB.setName(name); } catch {}
|
||||||
}
|
await colyseusService.setPlayerName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateColor() {
|
async function updateColor() {
|
||||||
@@ -174,6 +194,7 @@ async function updateColor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleQuickPlay() {
|
async function handleQuickPlay() {
|
||||||
|
if (!colyseusService.nameConfirmed.value) return;
|
||||||
isJoining.value = true;
|
isJoining.value = true;
|
||||||
console.log('Starting quickPlay...');
|
console.log('Starting quickPlay...');
|
||||||
try {
|
try {
|
||||||
@@ -197,6 +218,7 @@ async function handleQuickPlay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function joinRoom(roomId: string) {
|
async function joinRoom(roomId: string) {
|
||||||
|
if (!colyseusService.nameConfirmed.value) return;
|
||||||
isJoining.value = true;
|
isJoining.value = true;
|
||||||
try {
|
try {
|
||||||
// For direct room joining, we can use joinGameRoom directly
|
// For direct room joining, we can use joinGameRoom directly
|
||||||
@@ -460,4 +482,9 @@ async function joinRoom(roomId: string) {
|
|||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Additional guards and hints */
|
||||||
|
.room-card.disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
.hint { color:#666; font-size: 14px; margin-top: 10px; }
|
||||||
|
.hint.ok { color: #2e7d32; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,15 +32,11 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
|
|
||||||
onJoin(client: Client, options: any) {
|
onJoin(client: Client, options: any) {
|
||||||
console.log(`[LobbyRoom] ${client.sessionId} joined lobby`);
|
console.log(`[LobbyRoom] ${client.sessionId} joined lobby`);
|
||||||
|
// Do NOT assign a default name on join. Wait until client presses "Set Name".
|
||||||
const defaultName = `guest`;
|
this.state.addPlayer(client.sessionId, "");
|
||||||
const uniqueName = NameManager.getInstance().generateUniquePlayerName(defaultName, client.sessionId);
|
|
||||||
|
|
||||||
this.state.addPlayer(client.sessionId, uniqueName);
|
|
||||||
|
|
||||||
client.send("welcome", {
|
client.send("welcome", {
|
||||||
sessionId: client.sessionId,
|
sessionId: client.sessionId,
|
||||||
assignedName: uniqueName,
|
|
||||||
color: this.state.players.get(client.sessionId)?.color || "#667eea"
|
color: this.state.players.get(client.sessionId)?.color || "#667eea"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,6 +96,11 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
private async handleQuickPlay(client: Client) {
|
private async handleQuickPlay(client: Client) {
|
||||||
const player = this.state.players.get(client.sessionId);
|
const player = this.state.players.get(client.sessionId);
|
||||||
if (!player || player.inGame) return;
|
if (!player || player.inGame) return;
|
||||||
|
// Prevent players without a confirmed name from joining games
|
||||||
|
if (!player.name || !player.name.trim()) {
|
||||||
|
client.send("error", { message: "Please set a name before joining a game." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First try to find an available room
|
// First try to find an available room
|
||||||
@@ -138,6 +139,10 @@ export class LobbyRoom extends Room<LobbyState> {
|
|||||||
private async handleJoinRoom(client: Client, roomId: string) {
|
private async handleJoinRoom(client: Client, roomId: string) {
|
||||||
const player = this.state.players.get(client.sessionId);
|
const player = this.state.players.get(client.sessionId);
|
||||||
if (!player || player.inGame) return;
|
if (!player || player.inGame) return;
|
||||||
|
if (!player.name || !player.name.trim()) {
|
||||||
|
client.send("error", { message: "Please set a name before joining a game." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify the room exists and is available
|
// Verify the room exists and is available
|
||||||
|
|||||||
@@ -14,18 +14,27 @@ export class NameManager {
|
|||||||
|
|
||||||
generateUniquePlayerName(baseName: string, sessionId: string): string {
|
generateUniquePlayerName(baseName: string, sessionId: string): string {
|
||||||
const normalizedName = baseName.trim().toLowerCase();
|
const normalizedName = baseName.trim().toLowerCase();
|
||||||
|
|
||||||
if (!normalizedName) {
|
if (!normalizedName) {
|
||||||
return this.generateUniquePlayerName('player', sessionId);
|
// Default base name when none is provided
|
||||||
|
return this.generateUniquePlayerName('guest', sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCounter = this.nameCounters.get(normalizedName) || 0;
|
// Try exact name if not in use; otherwise, append incremental suffixes
|
||||||
const newCounter = currentCounter + 1;
|
const isInUse = (name: string) => {
|
||||||
this.nameCounters.set(normalizedName, newCounter);
|
for (const val of this.sessionToName.values()) {
|
||||||
|
if (val === name) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let uniqueName = normalizedName;
|
||||||
|
if (isInUse(uniqueName)) {
|
||||||
|
let n = 2;
|
||||||
|
while (isInUse(`${normalizedName}-${n}`)) n++;
|
||||||
|
uniqueName = `${normalizedName}-${n}`;
|
||||||
|
}
|
||||||
|
|
||||||
const uniqueName = newCounter === 1 ? normalizedName : `${normalizedName}-${newCounter}`;
|
|
||||||
this.sessionToName.set(sessionId, uniqueName);
|
this.sessionToName.set(sessionId, uniqueName);
|
||||||
|
|
||||||
return uniqueName;
|
return uniqueName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user