sistema de juego ajustado a evento CIAT
This commit is contained in:
BIN
Untitled.png
BIN
Untitled.png
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
@@ -1,31 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="game">
|
<div class="game">
|
||||||
<!-- End of game modal overlay -->
|
<GameEndModal
|
||||||
<div v-if="endModal.visible" class="end-modal-overlay" @click.self="dismissEndModal">
|
:visible="endModalVisible"
|
||||||
<div class="end-modal">
|
:final-scores="finalScores"
|
||||||
<button class="close-btn" @click="dismissEndModal" aria-label="Cerrar">×</button>
|
:variants="variants"
|
||||||
<div class="title">🏁 Juego finalizado</div>
|
:current-variant="currentVariant"
|
||||||
<div class="scores">
|
:round="modalRound !== null ? modalRound : currentRound"
|
||||||
<div v-for="s in finalScores" :key="s.sessionId" class="score-row">
|
:total-rounds="3"
|
||||||
<span class="name">{{ s.name }}</span>
|
@dismiss="dismissEndModal"
|
||||||
<span class="tokens">🦃 {{ s.pavo }} · 🌽 {{ s.elote }}</span>
|
@next-variant="changeToNextVariant"
|
||||||
<span class="points">Puntos: {{ s.points }}</span>
|
@previous-variant="changeToPreviousVariant"
|
||||||
</div>
|
@restart-variant="restartCurrentVariant"
|
||||||
</div>
|
/>
|
||||||
<div class="modal-actions">
|
|
||||||
<button @click="changeToPreviousVariant" class="btn btn-prev-variant">
|
|
||||||
⏪ {{ getPreviousVariant() }}
|
|
||||||
</button>
|
|
||||||
<button @click="restartCurrentVariant" class="btn btn-restart-variant">
|
|
||||||
🔄 {{ currentVariant }}
|
|
||||||
</button>
|
|
||||||
<button @click="changeToNextVariant" class="btn btn-next-variant">
|
|
||||||
{{ getNextVariant() }} ⏩
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="hint">Se cerrará en {{ remainingSeconds }}s</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
<div class="game-header">
|
<div class="game-header">
|
||||||
<h1>🧪 Demo Room</h1>
|
<h1>🧪 Demo Room</h1>
|
||||||
@@ -105,6 +91,7 @@ import G4 from './games/G4.vue';
|
|||||||
import G5 from './games/G5.vue';
|
import G5 from './games/G5.vue';
|
||||||
import PlayerStats from './games/PlayerStats.vue';
|
import PlayerStats from './games/PlayerStats.vue';
|
||||||
import ChatWidget from './games/ChatWidget.vue';
|
import ChatWidget from './games/ChatWidget.vue';
|
||||||
|
import GameEndModal from './games/GameEndModal.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -125,32 +112,10 @@ const outcomeP2 = ref(0);
|
|||||||
|
|
||||||
const variants = ['G1','G2','G3','G4','G5'];
|
const variants = ['G1','G2','G3','G4','G5'];
|
||||||
|
|
||||||
// End-of-game modal state and helpers
|
// End-of-game modal visibility
|
||||||
const endModal = ref<{ visible: boolean }>({ visible: false });
|
const endModalVisible = ref(false);
|
||||||
const remainingSeconds = ref(20);
|
function showEndModal() { if (!endModalVisible.value) endModalVisible.value = true; }
|
||||||
let endTimerTimeout: any = null;
|
function dismissEndModal() { endModalVisible.value = false; modalScoresOverride.value = null; modalRound.value = null; }
|
||||||
let endTimerInterval: any = null;
|
|
||||||
|
|
||||||
function showEndModal() {
|
|
||||||
// Prevent multiple timers
|
|
||||||
if (endModal.value.visible) return;
|
|
||||||
endModal.value.visible = true;
|
|
||||||
remainingSeconds.value = 20;
|
|
||||||
if (endTimerInterval) clearInterval(endTimerInterval);
|
|
||||||
if (endTimerTimeout) clearTimeout(endTimerTimeout);
|
|
||||||
endTimerInterval = setInterval(() => {
|
|
||||||
remainingSeconds.value = Math.max(0, remainingSeconds.value - 1);
|
|
||||||
}, 1000);
|
|
||||||
endTimerTimeout = setTimeout(() => {
|
|
||||||
dismissEndModal();
|
|
||||||
}, 20000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissEndModal() {
|
|
||||||
endModal.value.visible = false;
|
|
||||||
if (endTimerInterval) { clearInterval(endTimerInterval); endTimerInterval = null; }
|
|
||||||
if (endTimerTimeout) { clearTimeout(endTimerTimeout); endTimerTimeout = null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get next variant in sequence
|
// Function to get next variant in sequence
|
||||||
function getNextVariant(): string {
|
function getNextVariant(): string {
|
||||||
@@ -186,19 +151,32 @@ function restartCurrentVariant() {
|
|||||||
dismissEndModal();
|
dismissEndModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modal score override for round-end summaries from server
|
||||||
|
const modalScoresOverride = ref<any[] | null>(null);
|
||||||
|
const modalRound = ref<number | null>(null);
|
||||||
|
|
||||||
const finalScores = computed(() => {
|
const finalScores = computed(() => {
|
||||||
return players.value.map(p => {
|
// If server sent a round summary, use that to keep values even if tokens reset
|
||||||
const points = (p.role === 'P2')
|
if (modalScoresOverride.value && Array.isArray(modalScoresOverride.value)) {
|
||||||
? (p.eloteTokens || 0) * 1 + (p.pavoTokens || 0) * 2
|
return modalScoresOverride.value;
|
||||||
: (p.pavoTokens || 0) * 1 + (p.eloteTokens || 0) * 2;
|
}
|
||||||
return {
|
// Fallback: compute from current player tokens
|
||||||
sessionId: p.sessionId,
|
return players.value
|
||||||
name: p.name,
|
.map(p => {
|
||||||
pavo: p.pavoTokens || 0,
|
const points = (p.role === 'P2')
|
||||||
elote: p.eloteTokens || 0,
|
? (p.eloteTokens || 0) * 1 + (p.pavoTokens || 0) * 2
|
||||||
points
|
: (p.pavoTokens || 0) * 1 + (p.eloteTokens || 0) * 2;
|
||||||
};
|
return {
|
||||||
}).sort((a, b) => b.points - a.points);
|
sessionId: p.sessionId,
|
||||||
|
name: p.name,
|
||||||
|
role: p.role,
|
||||||
|
pavo: p.pavoTokens || 0,
|
||||||
|
elote: p.eloteTokens || 0,
|
||||||
|
points,
|
||||||
|
color: p.color
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.points - a.points);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Round transition banner state and helper
|
// Round transition banner state and helper
|
||||||
@@ -303,7 +281,20 @@ onMounted(() => {
|
|||||||
colyseusService.playerName.value = info.name;
|
colyseusService.playerName.value = info.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
room.onMessage("gameEnd", () => { showEndModal(); });
|
room.onMessage("gameEnd", () => { modalRound.value = currentRound.value; showEndModal(); });
|
||||||
|
room.onMessage("roundEnded", (payload: any) => {
|
||||||
|
// Use the server-provided summary to render the modal between rounds
|
||||||
|
if (payload && Array.isArray(payload.scores)) {
|
||||||
|
modalScoresOverride.value = payload.scores;
|
||||||
|
}
|
||||||
|
if (payload && typeof payload.round === 'number') {
|
||||||
|
modalRound.value = payload.round;
|
||||||
|
} else {
|
||||||
|
modalRound.value = currentRound.value;
|
||||||
|
}
|
||||||
|
showEndModal();
|
||||||
|
});
|
||||||
|
// Do not auto-dismiss on roundStarted; let the modal's timer or user close it
|
||||||
|
|
||||||
// Register additional message handlers to avoid warnings
|
// Register additional message handlers to avoid warnings
|
||||||
room.onMessage("gamePaused", () => {
|
room.onMessage("gamePaused", () => {
|
||||||
@@ -317,7 +308,7 @@ onMounted(() => {
|
|||||||
room.onMessage("variantChanged", (data: { variant: string }) => {
|
room.onMessage("variantChanged", (data: { variant: string }) => {
|
||||||
currentVariant.value = data.variant as any;
|
currentVariant.value = data.variant as any;
|
||||||
// Close end modal if it's open when variant changes
|
// Close end modal if it's open when variant changes
|
||||||
if (endModal.value.visible) {
|
if (endModalVisible.value) {
|
||||||
dismissEndModal();
|
dismissEndModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -417,57 +408,6 @@ async function leaveGame() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.game { min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display:flex; align-items:center; justify-content:center; padding:20px; }
|
.game { min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display:flex; align-items:center; justify-content:center; padding:20px; }
|
||||||
.end-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; z-index: 1200; }
|
|
||||||
.end-modal { position: relative; background: white; color:#111; border-radius: 16px; padding: 24px 24px 18px; width: min(520px, 92vw); box-shadow: 0 30px 80px rgba(0,0,0,0.5); border:1px solid #e5e7eb; }
|
|
||||||
.end-modal .close-btn { position:absolute; top:8px; right:8px; width:32px; height:32px; border-radius: 8px; border:1px solid #e5e7eb; background:#f8fafc; color:#111; font-weight:800; cursor:pointer; }
|
|
||||||
.end-modal .close-btn:hover { background:#eef2ff; }
|
|
||||||
.end-modal .title { font-size: 20px; font-weight: 900; margin-bottom: 8px; }
|
|
||||||
.end-modal .scores { display:flex; flex-direction:column; gap:8px; margin: 8px 0 12px; }
|
|
||||||
.end-modal .score-row { display:flex; align-items:center; justify-content:space-between; gap:8px; background:#f8fafc; border:1px solid #e5e7eb; border-radius: 10px; padding:8px 10px; }
|
|
||||||
.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-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; }
|
.game-header { display:flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||||
.game-header h1 { margin: 0; font-size: 20px; }
|
.game-header h1 { margin: 0; font-size: 20px; }
|
||||||
|
|||||||
246
client/src/views/games/GameEndModal.vue
Normal file
246
client/src/views/games/GameEndModal.vue
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="end-modal-overlay" @click.self="onDismiss">
|
||||||
|
<div class="end-modal">
|
||||||
|
<button class="close-btn" @click="onDismiss" aria-label="Cerrar">×</button>
|
||||||
|
<div class="title">
|
||||||
|
<template v-if="isFinal">🏁 Juego finalizado</template>
|
||||||
|
<template v-else>Resultados Ronda {{ round }} de {{ totalRounds }}</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scores">
|
||||||
|
<div
|
||||||
|
v-for="s in finalScores"
|
||||||
|
:key="s.sessionId"
|
||||||
|
class="score-row"
|
||||||
|
:style="({ '--primary': s.color || '#667eea' }) as any"
|
||||||
|
>
|
||||||
|
<div class="left">
|
||||||
|
<span class="color-dot" :style="{ background: s.color || '#667eea' }"></span>
|
||||||
|
<span class="name">{{ s.name }}</span>
|
||||||
|
<span v-if="s.role" class="role" :class="s.role">{{ s.role }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<span class="tokens">🦃 {{ s.pavo }} · 🌽 {{ s.elote }}</span>
|
||||||
|
<span class="points">{{ s.points }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="isFinal">
|
||||||
|
<div v-if="adminUnlocked" class="modal-actions">
|
||||||
|
<button @click="onPrev" class="btn btn-prev-variant">
|
||||||
|
⏪ {{ previousVariantLabel }}
|
||||||
|
</button>
|
||||||
|
<button @click="onRestart" class="btn btn-restart-variant">
|
||||||
|
🔄 {{ currentVariant }}
|
||||||
|
</button>
|
||||||
|
<button @click="onNext" class="btn btn-next-variant">
|
||||||
|
{{ nextVariantLabel }} ⏩
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="round-info clickable" @click="onRoundInfoClick" :title="roundInfoTitle">
|
||||||
|
(espere a que el administrador continue la partida)
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="round-info">
|
||||||
|
Ronda {{ round }} de {{ totalRounds }} — aún quedan rondas por jugar. La siguiente comenzará en breve.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hint">Se cerrará en {{ remainingSeconds }}s</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
interface Score {
|
||||||
|
sessionId: string;
|
||||||
|
name: string;
|
||||||
|
role?: 'P1' | 'P2' | '';
|
||||||
|
pavo: number;
|
||||||
|
elote: number;
|
||||||
|
points: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
finalScores: Score[];
|
||||||
|
variants: string[];
|
||||||
|
currentVariant: string;
|
||||||
|
round?: number;
|
||||||
|
totalRounds?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'dismiss'): void;
|
||||||
|
(e: 'next-variant'): void;
|
||||||
|
(e: 'previous-variant'): void;
|
||||||
|
(e: 'restart-variant'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const remainingSeconds = ref(20);
|
||||||
|
let intervalId: any = null;
|
||||||
|
let timeoutId: any = null;
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
stopTimer();
|
||||||
|
remainingSeconds.value = 20;
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
remainingSeconds.value = Math.max(0, remainingSeconds.value - 1);
|
||||||
|
}, 1000);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
onDismiss();
|
||||||
|
}, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (intervalId) { clearInterval(intervalId); intervalId = null; }
|
||||||
|
if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.visible, (v) => {
|
||||||
|
if (v) startTimer();
|
||||||
|
else stopTimer();
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onBeforeUnmount(() => { stopTimer(); });
|
||||||
|
|
||||||
|
const totalRounds = computed(() => Math.max(1, Number(props.totalRounds || 3)));
|
||||||
|
const round = computed(() => Math.max(1, Number(props.round || 1)));
|
||||||
|
const isFinal = computed(() => round.value >= totalRounds.value);
|
||||||
|
|
||||||
|
// Hidden admin unlock via 5 rapid clicks on the round info text
|
||||||
|
const adminUnlocked = ref(false);
|
||||||
|
const clickCount = ref(0);
|
||||||
|
let clickResetTimer: any = null;
|
||||||
|
|
||||||
|
function onRoundInfoClick() {
|
||||||
|
if (clickResetTimer) { clearTimeout(clickResetTimer); clickResetTimer = null; }
|
||||||
|
clickCount.value += 1;
|
||||||
|
if (clickCount.value >= 5) {
|
||||||
|
adminUnlocked.value = true;
|
||||||
|
} else {
|
||||||
|
// Small window to keep clicks "seguido"
|
||||||
|
clickResetTimer = setTimeout(() => { clickCount.value = 0; }, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundInfoTitle = computed(() => adminUnlocked.value ? 'Controles de variante desbloqueados' : `Clicks: ${clickCount.value}/5 para desbloquear`);
|
||||||
|
|
||||||
|
watch(() => props.visible, (v) => {
|
||||||
|
if (v) {
|
||||||
|
adminUnlocked.value = false;
|
||||||
|
clickCount.value = 0;
|
||||||
|
if (clickResetTimer) { clearTimeout(clickResetTimer); clickResetTimer = null; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextVariantLabel = computed(() => {
|
||||||
|
const list = props.variants || [];
|
||||||
|
const i = Math.max(0, list.indexOf(props.currentVariant));
|
||||||
|
const next = (i + 1) % Math.max(1, list.length);
|
||||||
|
return list[next] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const previousVariantLabel = computed(() => {
|
||||||
|
const list = props.variants || [];
|
||||||
|
const i = Math.max(0, list.indexOf(props.currentVariant));
|
||||||
|
const prev = (i - 1 + Math.max(1, list.length)) % Math.max(1, list.length);
|
||||||
|
return list[prev] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
function onDismiss() { emit('dismiss'); }
|
||||||
|
function onNext() { emit('next-variant'); }
|
||||||
|
function onPrev() { emit('previous-variant'); }
|
||||||
|
function onRestart() { emit('restart-variant'); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.end-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.40); display:flex; align-items:center; justify-content:center; z-index: 1200; }
|
||||||
|
.end-modal {
|
||||||
|
position: relative;
|
||||||
|
color:#111;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px 24px 18px;
|
||||||
|
width: min(520px, 92vw);
|
||||||
|
/* Glassmorphism mejorado: más blanco pero transparente */
|
||||||
|
background: rgba(255, 255, 255, 0.784);
|
||||||
|
border: 1px solid rgba(255,255,255,0.8);
|
||||||
|
box-shadow: 0 16px 32px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.6);
|
||||||
|
backdrop-filter: blur(20px) saturate(120%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(120%);
|
||||||
|
}
|
||||||
|
.end-modal .close-btn { position:absolute; top:8px; right:8px; width:32px; height:32px; border-radius: 8px; border:1px solid #e5e7eb; background:#f8fafc; color:#111; font-weight:800; cursor:pointer; }
|
||||||
|
.end-modal .close-btn:hover { background:#eef2ff; }
|
||||||
|
.end-modal .title { font-size: 20px; font-weight: 900; margin-bottom: 8px; }
|
||||||
|
.end-modal .scores { display:flex; flex-direction:column; gap:8px; margin: 8px 0 12px; }
|
||||||
|
.end-modal .score-row {
|
||||||
|
display:flex; align-items:center; justify-content:space-between; gap:8px;
|
||||||
|
background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 6%, white) 0%, #ffffff 100%);
|
||||||
|
border:1px solid color-mix(in srgb, var(--primary) 20%, #e6e9ff);
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
border-radius: 10px; padding:8px 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
.end-modal .score-row:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
.end-modal .score-row .left { display:flex; align-items:center; gap:8px; }
|
||||||
|
.end-modal .score-row .right { display:flex; align-items:center; gap:20px; }
|
||||||
|
.end-modal .score-row .tokens { margin-right: 12px; }
|
||||||
|
.end-modal .score-row .color-dot { width:10px; height:10px; border-radius:50%; box-shadow: 0 0 0 2px #fff inset; }
|
||||||
|
.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: var(--primary); }
|
||||||
|
.end-modal .score-row .role { font-size:12px; padding:2px 8px; border-radius:10px; background:#f0f0f0; color:#555; }
|
||||||
|
.end-modal .score-row .role.P1,
|
||||||
|
.end-modal .score-row .role.P2 { background: color-mix(in srgb, var(--primary) 15%, white); color: var(--primary); }
|
||||||
|
.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; }
|
||||||
|
.round-info { margin: 10px 2px 2px; font-size: 13px; font-weight:600; color:#334155; text-align:center; }
|
||||||
|
.round-info.clickable { cursor: pointer; user-select: none; }
|
||||||
|
.round-info.clickable:hover { filter: brightness(0.95); }
|
||||||
|
</style>
|
||||||
@@ -519,6 +519,41 @@ export class GameRoom extends Room<GameState> {
|
|||||||
broadcastDashboardUpdate();
|
broadcastDashboardUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildRoundSummary() {
|
||||||
|
const scores: any[] = [];
|
||||||
|
this.state.players.forEach((p, key) => {
|
||||||
|
const pavo = p.pavoTokens || 0;
|
||||||
|
const elote = p.eloteTokens || 0;
|
||||||
|
const points = (p.role === 'P2') ? (elote * 1 + pavo * 2) : (pavo * 1 + elote * 2);
|
||||||
|
scores.push({
|
||||||
|
sessionId: p.sessionId,
|
||||||
|
name: p.name,
|
||||||
|
role: p.role,
|
||||||
|
pavo,
|
||||||
|
elote,
|
||||||
|
points,
|
||||||
|
color: (p as any).color,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Highest score first
|
||||||
|
scores.sort((a, b) => b.points - a.points);
|
||||||
|
return {
|
||||||
|
round: this.state.currentRound,
|
||||||
|
variant: this.state.currentVariant,
|
||||||
|
scores,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetTokensForNewRound() {
|
||||||
|
// Preserve shame tokens but reset pavo/elote according to role
|
||||||
|
const p1 = this.state.p1Id ? this.state.players.get(this.state.p1Id) : undefined;
|
||||||
|
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
|
||||||
|
if (p1) { p1.pavoTokens = 10; p1.eloteTokens = 0; }
|
||||||
|
if (p2) { p2.pavoTokens = 0; p2.eloteTokens = 10; }
|
||||||
|
// Notify dashboard of token reset
|
||||||
|
broadcastDashboardUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
private resolveP2Action() {
|
private resolveP2Action() {
|
||||||
const p1 = this.state.p1Id ? this.state.players.get(this.state.p1Id) : undefined;
|
const p1 = this.state.p1Id ? this.state.players.get(this.state.p1Id) : undefined;
|
||||||
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
|
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
|
||||||
@@ -914,7 +949,13 @@ export class GameRoom extends Room<GameState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private advanceRound() {
|
private advanceRound() {
|
||||||
|
// Broadcast end-of-round summary BEFORE any resets so clients can render results
|
||||||
|
const summary = this.buildRoundSummary();
|
||||||
|
this.broadcast("roundEnded", summary);
|
||||||
|
|
||||||
if (this.state.currentRound < 3) {
|
if (this.state.currentRound < 3) {
|
||||||
|
// Prepare next round: reset tokens and round decisions
|
||||||
|
this.resetTokensForNewRound();
|
||||||
this.state.currentRound += 1;
|
this.state.currentRound += 1;
|
||||||
this.state.resetRound();
|
this.state.resetRound();
|
||||||
// Update metadata with new round
|
// Update metadata with new round
|
||||||
@@ -928,6 +969,7 @@ export class GameRoom extends Room<GameState> {
|
|||||||
// Notify dashboard of round advance
|
// Notify dashboard of round advance
|
||||||
broadcastDashboardUpdate();
|
broadcastDashboardUpdate();
|
||||||
} else {
|
} else {
|
||||||
|
// Final round finished: finish the game
|
||||||
this.state.finishGame();
|
this.state.finishGame();
|
||||||
this.endGame();
|
this.endGame();
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user