diff --git a/client/src/views/DemoGame.vue b/client/src/views/DemoGame.vue index 8ec2533..1a217f9 100644 --- a/client/src/views/DemoGame.vue +++ b/client/src/views/DemoGame.vue @@ -12,6 +12,17 @@ Puntos: {{ s.points }} +
Se cerrará en {{ remainingSeconds }}s
@@ -116,7 +127,7 @@ const variants = ['G1','G2','G3','G4','G5']; // End-of-game modal state and helpers const endModal = ref<{ visible: boolean }>({ visible: false }); -const remainingSeconds = ref(10); +const remainingSeconds = ref(20); let endTimerTimeout: any = null; let endTimerInterval: any = null; @@ -124,7 +135,7 @@ function showEndModal() { // Prevent multiple timers if (endModal.value.visible) return; endModal.value.visible = true; - remainingSeconds.value = 10; + remainingSeconds.value = 20; if (endTimerInterval) clearInterval(endTimerInterval); if (endTimerTimeout) clearTimeout(endTimerTimeout); endTimerInterval = setInterval(() => { @@ -132,7 +143,7 @@ function showEndModal() { }, 1000); endTimerTimeout = setTimeout(() => { dismissEndModal(); - }, 10000); + }, 20000); } function dismissEndModal() { @@ -141,6 +152,40 @@ function dismissEndModal() { if (endTimerTimeout) { clearTimeout(endTimerTimeout); endTimerTimeout = null; } } +// Function to get next variant in sequence +function getNextVariant(): string { + const currentIndex = variants.indexOf(currentVariant.value); + const nextIndex = (currentIndex + 1) % variants.length; + return variants[nextIndex]; +} + +// Function to get previous variant in sequence +function getPreviousVariant(): string { + const currentIndex = variants.indexOf(currentVariant.value); + const previousIndex = (currentIndex - 1 + variants.length) % variants.length; + return variants[previousIndex]; +} + +// Function to change to next variant and dismiss modal +function changeToNextVariant() { + const nextVariant = getNextVariant(); + setVariant(nextVariant); + dismissEndModal(); +} + +// Function to change to previous variant and dismiss modal +function changeToPreviousVariant() { + const previousVariant = getPreviousVariant(); + setVariant(previousVariant); + dismissEndModal(); +} + +// Function to restart the same variant and dismiss modal +function restartCurrentVariant() { + setVariant(currentVariant.value); + dismissEndModal(); +} + const finalScores = computed(() => { return players.value.map(p => { const points = (p.role === 'P2') @@ -271,6 +316,10 @@ onMounted(() => { room.onMessage("variantChanged", (data: { variant: string }) => { currentVariant.value = data.variant as any; + // Close end modal if it's open when variant changes + if (endModal.value.visible) { + dismissEndModal(); + } }); // No round transition banners @@ -378,6 +427,46 @@ async function leaveGame() { .end-modal .score-row .name { font-weight:800; color:#1f2937; } .end-modal .score-row .tokens { font-weight:700; color:#374151; } .end-modal .score-row .points { font-weight:900; color:#111827; } +.end-modal .modal-actions { + margin: 16px 0; + display: flex; + gap: 12px; + justify-content: center; + align-items: center; +} +.end-modal .btn-next-variant, .end-modal .btn-prev-variant, .end-modal .btn-restart-variant { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + min-width: 85px; +} +.end-modal .btn-prev-variant { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + box-shadow: 0 4px 12px rgba(240, 147, 251, 0.3); +} +.end-modal .btn-restart-variant { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + box-shadow: 0 4px 12px rgba(79, 172, 254, 0.3); +} +.end-modal .btn-next-variant:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); +} +.end-modal .btn-prev-variant:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(240, 147, 251, 0.4); +} +.end-modal .btn-restart-variant:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(79, 172, 254, 0.4); +} .end-modal .hint { font-size: 12px; color:#6b7280; text-align:right; } .game-container { background: white; border-radius: 20px; padding: 24px; max-width: 1000px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } .game-header { display:flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; } diff --git a/client/src/views/games/OfferControls.vue b/client/src/views/games/OfferControls.vue index 3712ee5..b2e6db9 100644 --- a/client/src/views/games/OfferControls.vue +++ b/client/src/views/games/OfferControls.vue @@ -115,15 +115,20 @@ const requestElote = ref(0); const advancedMode = ref(false); // Start in basic mode const room = computed(() => colyseusService.gameRoom.value as any); -const p1 = computed(() => room.value?.state ? room.value.state.players.get(room.value.state.p1Id) : null); -const p2 = computed(() => room.value?.state ? room.value.state.players.get(room.value.state.p2Id) : null); const isFinished = ref(false); -const maxOfferPavo = computed(() => (p1.value?.pavoTokens ?? Infinity)); -const maxOfferElote = computed(() => (p1.value?.eloteTokens ?? Infinity)); -const maxRequestPavo = computed(() => (p2.value?.pavoTokens ?? Infinity)); -const maxRequestElote = computed(() => (p2.value?.eloteTokens ?? Infinity)); + +// Reactive refs for player tokens +const p1PavoTokens = ref(0); +const p1EloteTokens = ref(0); +const p2PavoTokens = ref(0); +const p2EloteTokens = ref(0); + +const maxOfferPavo = computed(() => p1PavoTokens.value); +const maxOfferElote = computed(() => p1EloteTokens.value); +const maxRequestPavo = computed(() => p2PavoTokens.value); +const maxRequestElote = computed(() => p2EloteTokens.value); const isNonsense = computed(() => (offerPavo.value|0) === (requestPavo.value|0) && (offerElote.value|0) === (requestElote.value|0)); -const canMakeBasicOffer = computed(() => (p1.value?.pavoTokens ?? 0) >= 3); +const canMakeBasicOffer = computed(() => p1PavoTokens.value >= 3); function clampAll() { offerPavo.value = Math.max(0, Math.min(offerPavo.value | 0, maxOfferPavo.value)); @@ -138,9 +143,64 @@ onMounted(() => { if (r?.state) { isFinished.value = ((r.state.gameStatus || '').toLowerCase() === 'finished'); const $ = getStateCallbacks(r); + + // Initialize token values + const p1 = r.state.players.get(r.state.p1Id); + const p2 = r.state.players.get(r.state.p2Id); + if (p1) { + p1PavoTokens.value = p1.pavoTokens || 0; + p1EloteTokens.value = p1.eloteTokens || 0; + } + if (p2) { + p2PavoTokens.value = p2.pavoTokens || 0; + p2EloteTokens.value = p2.eloteTokens || 0; + } + $(r.state).listen('gameStatus', (v: string) => { isFinished.value = (v || '').toLowerCase() === 'finished'; }); + + // Reset inputs when round goes back to 1 (game restart) + $(r.state).listen('currentRound', (round: number) => { + if (round === 1) { + offerPavo.value = 0; + offerElote.value = 0; + requestPavo.value = 0; + requestElote.value = 0; + } + }); + + // Update token refs when player tokens change + $(r.state).players.onAdd((player: any, sessionId: string) => { + const isP1 = sessionId === r.state.p1Id; + const isP2 = sessionId === r.state.p2Id; + + // Set initial values + if (isP1) { + p1PavoTokens.value = player.pavoTokens || 0; + p1EloteTokens.value = player.eloteTokens || 0; + } else if (isP2) { + p2PavoTokens.value = player.pavoTokens || 0; + p2EloteTokens.value = player.eloteTokens || 0; + } + + $(player).listen('pavoTokens', (tokens: number) => { + if (isP1) { + p1PavoTokens.value = tokens || 0; + } else if (isP2) { + p2PavoTokens.value = tokens || 0; + } + clampAll(); + }); + $(player).listen('eloteTokens', (tokens: number) => { + if (isP1) { + p1EloteTokens.value = tokens || 0; + } else if (isP2) { + p2EloteTokens.value = tokens || 0; + } + clampAll(); + }); + }); } }); diff --git a/server/src/rooms/GameRoom.ts b/server/src/rooms/GameRoom.ts index ffdd2b2..9450dc4 100644 --- a/server/src/rooms/GameRoom.ts +++ b/server/src/rooms/GameRoom.ts @@ -77,18 +77,45 @@ export class GameRoom extends Room { // Reset to round 1 and clear decisions when variant changes this.state.currentRound = 1; this.state.resetRound(); - // Update metadata with new variant and round (don't special-case FINISHED) + + // Reset player tokens while preserving shame tokens + this.state.players.forEach((player, sessionId) => { + const currentShameTokens = player.shameTokens || 0; + + if (player.role === 'P1') { + player.pavoTokens = 10; + player.eloteTokens = 0; + } else if (player.role === 'P2') { + player.pavoTokens = 0; + player.eloteTokens = 10; + } + + // Preserve shame tokens + player.shameTokens = currentShameTokens; + }); + + // If game was finished, restart it + if (this.state.gameStatus === GameStatus.FINISHED) { + this.state.gameStatus = GameStatus.PLAYING; + } + + // Update metadata with new status + const statusString = this.state.gameStatus === GameStatus.WAITING ? 'waiting' : + (this.state.gameStatus === GameStatus.PAUSED ? 'paused' : + (this.state.gameStatus === GameStatus.FINISHED ? 'finished' : 'playing')); + this.setMetadata({ - gameStatus: this.state.gameStatus === GameStatus.WAITING ? 'waiting' : (this.state.gameStatus === GameStatus.PAUSED ? 'paused' : (this.state.gameStatus === GameStatus.FINISHED ? 'finished' : 'playing')), + gameStatus: statusString, currentRound: this.state.currentRound, currentVariant: this.state.currentVariant }); + // G2: Force offer by default if (variant === 'G2') { this.state.forcedByP2 = true; } this.broadcast("variantChanged", { variant }); - this.sysChat(`🔄 Variante cambiada a ${variant}`, 'variant_change'); + this.sysChat(`🔄 Variante cambiada a ${variant} - Juego reiniciado`, 'variant_change'); }); // P1 proposes a variable offer (offer -> P2, request <- from P2)