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>
|
||||
<div class="game">
|
||||
<!-- End of game modal overlay -->
|
||||
<div v-if="endModal.visible" class="end-modal-overlay" @click.self="dismissEndModal">
|
||||
<div class="end-modal">
|
||||
<button class="close-btn" @click="dismissEndModal" aria-label="Cerrar">×</button>
|
||||
<div class="title">🏁 Juego finalizado</div>
|
||||
<div class="scores">
|
||||
<div v-for="s in finalScores" :key="s.sessionId" class="score-row">
|
||||
<span class="name">{{ s.name }}</span>
|
||||
<span class="tokens">🦃 {{ s.pavo }} · 🌽 {{ s.elote }}</span>
|
||||
<span class="points">Puntos: {{ s.points }}</span>
|
||||
</div>
|
||||
</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>
|
||||
<GameEndModal
|
||||
:visible="endModalVisible"
|
||||
:final-scores="finalScores"
|
||||
:variants="variants"
|
||||
:current-variant="currentVariant"
|
||||
:round="modalRound !== null ? modalRound : currentRound"
|
||||
:total-rounds="3"
|
||||
@dismiss="dismissEndModal"
|
||||
@next-variant="changeToNextVariant"
|
||||
@previous-variant="changeToPreviousVariant"
|
||||
@restart-variant="restartCurrentVariant"
|
||||
/>
|
||||
<div class="game-container">
|
||||
<div class="game-header">
|
||||
<h1>🧪 Demo Room</h1>
|
||||
@@ -105,6 +91,7 @@ import G4 from './games/G4.vue';
|
||||
import G5 from './games/G5.vue';
|
||||
import PlayerStats from './games/PlayerStats.vue';
|
||||
import ChatWidget from './games/ChatWidget.vue';
|
||||
import GameEndModal from './games/GameEndModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -125,32 +112,10 @@ const outcomeP2 = ref(0);
|
||||
|
||||
const variants = ['G1','G2','G3','G4','G5'];
|
||||
|
||||
// End-of-game modal state and helpers
|
||||
const endModal = ref<{ visible: boolean }>({ visible: false });
|
||||
const remainingSeconds = ref(20);
|
||||
let endTimerTimeout: any = 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; }
|
||||
}
|
||||
// End-of-game modal visibility
|
||||
const endModalVisible = ref(false);
|
||||
function showEndModal() { if (!endModalVisible.value) endModalVisible.value = true; }
|
||||
function dismissEndModal() { endModalVisible.value = false; modalScoresOverride.value = null; modalRound.value = null; }
|
||||
|
||||
// Function to get next variant in sequence
|
||||
function getNextVariant(): string {
|
||||
@@ -186,19 +151,32 @@ function restartCurrentVariant() {
|
||||
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(() => {
|
||||
return players.value.map(p => {
|
||||
const points = (p.role === 'P2')
|
||||
? (p.eloteTokens || 0) * 1 + (p.pavoTokens || 0) * 2
|
||||
: (p.pavoTokens || 0) * 1 + (p.eloteTokens || 0) * 2;
|
||||
return {
|
||||
sessionId: p.sessionId,
|
||||
name: p.name,
|
||||
pavo: p.pavoTokens || 0,
|
||||
elote: p.eloteTokens || 0,
|
||||
points
|
||||
};
|
||||
}).sort((a, b) => b.points - a.points);
|
||||
// If server sent a round summary, use that to keep values even if tokens reset
|
||||
if (modalScoresOverride.value && Array.isArray(modalScoresOverride.value)) {
|
||||
return modalScoresOverride.value;
|
||||
}
|
||||
// Fallback: compute from current player tokens
|
||||
return players.value
|
||||
.map(p => {
|
||||
const points = (p.role === 'P2')
|
||||
? (p.eloteTokens || 0) * 1 + (p.pavoTokens || 0) * 2
|
||||
: (p.pavoTokens || 0) * 1 + (p.eloteTokens || 0) * 2;
|
||||
return {
|
||||
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
|
||||
@@ -303,7 +281,20 @@ onMounted(() => {
|
||||
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
|
||||
room.onMessage("gamePaused", () => {
|
||||
@@ -317,7 +308,7 @@ 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) {
|
||||
if (endModalVisible.value) {
|
||||
dismissEndModal();
|
||||
}
|
||||
});
|
||||
@@ -417,57 +408,6 @@ async function leaveGame() {
|
||||
|
||||
<style scoped>
|
||||
.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-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; }
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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() {
|
||||
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;
|
||||
@@ -914,7 +949,13 @@ export class GameRoom extends Room<GameState> {
|
||||
}
|
||||
|
||||
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) {
|
||||
// Prepare next round: reset tokens and round decisions
|
||||
this.resetTokensForNewRound();
|
||||
this.state.currentRound += 1;
|
||||
this.state.resetRound();
|
||||
// Update metadata with new round
|
||||
@@ -928,6 +969,7 @@ export class GameRoom extends Room<GameState> {
|
||||
// Notify dashboard of round advance
|
||||
broadcastDashboardUpdate();
|
||||
} else {
|
||||
// Final round finished: finish the game
|
||||
this.state.finishGame();
|
||||
this.endGame();
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user