mejoras UI OfferControls

This commit is contained in:
2025-08-10 20:30:18 -06:00
parent bec944af4f
commit 8a7f97270d
7 changed files with 113 additions and 33 deletions

View File

@@ -195,11 +195,11 @@ function leaveGame() { colyseusService.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; }
.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; } .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; }
.meta { display:flex; gap: 16px; font-size: 14px; } .meta { display:flex; gap: 16px; font-size: 14px; }
.badge { background:#e3f2fd; color:#2196f3; padding: 2px 8px; border-radius: 12px; font-size: 12px; } .badge { background:#e3f2fd; color:#2196f3; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
.variant-selector { display:flex; gap: 8px; } .variant-selector { display:flex; gap: 8px; flex-wrap: wrap; }
.btn { padding: 8px 12px; border-radius: 8px; border: none; cursor: pointer; } .btn { padding: 8px 12px; border-radius: 8px; border: none; cursor: pointer; }
.btn-variant { background: #f2f2f2; } .btn-variant { background: #f2f2f2; }
.btn-variant.active { background: #667eea; color: white; } .btn-variant.active { background: #667eea; color: white; }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="g"> <div class="g">
<h3>G1 Sin derechos de propiedad</h3> <h3>G1 Sin derechos de propiedad</h3>
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/> <OfferControls v-if="myRole==='P1' && !state.offer?.active" :my-role="myRole" @propose="onPropose" @no-offer="onNoOffer"/>
<div v-if="state.offer?.active && !state.p2Action" class="controls"> <div v-if="state.offer?.active && !state.p2Action" class="controls">
<div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div> <div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div>
<div v-if="myRole === 'P2'"> <div v-if="myRole === 'P2'">

View File

@@ -4,7 +4,7 @@
<div class="controls" v-if="myRole === 'P2'"> <div class="controls" v-if="myRole === 'P2'">
<label><input type="checkbox" :checked="state.forcedByP2" @change="$emit('p2Force', ($event.target as HTMLInputElement).checked)"/> Forzar oferta</label> <label><input type="checkbox" :checked="state.forcedByP2" @change="$emit('p2Force', ($event.target as HTMLInputElement).checked)"/> Forzar oferta</label>
</div> </div>
<OfferControls v-if="myRole==='P1' && !state.offer?.active" :disable-no-offer="state.forcedByP2" @propose="onPropose" @no-offer="onNoOffer"/> <OfferControls v-if="myRole==='P1' && !state.offer?.active" :my-role="myRole" :disable-no-offer="state.forcedByP2" @propose="onPropose" @no-offer="onNoOffer"/>
<div v-if="state.offer?.active && !state.p2Action" class="controls"> <div v-if="state.offer?.active && !state.p2Action" class="controls">
<div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div> <div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div>
<div v-if="myRole === 'P2'"> <div v-if="myRole === 'P2'">

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="g"> <div class="g">
<h3>G3 Token de repudio (vergüenza)</h3> <h3>G3 Token de repudio (vergüenza)</h3>
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/> <OfferControls v-if="myRole==='P1' && !state.offer?.active" :my-role="myRole" @propose="onPropose" @no-offer="onNoOffer"/>
<div v-if="state.offer?.active && !state.p2Action" class="controls"> <div v-if="state.offer?.active && !state.p2Action" class="controls">
<div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div> <div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div>
<div v-if="myRole === 'P2'"> <div v-if="myRole === 'P2'">

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="g"> <div class="g">
<h3>G4 Derechos mínimos de propiedad (juez)</h3> <h3>G4 Derechos mínimos de propiedad (juez)</h3>
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/> <OfferControls v-if="myRole==='P1' && !state.offer?.active" :my-role="myRole" @propose="onPropose" @no-offer="onNoOffer"/>
<div v-if="state.offer?.active && !state.p2Action" class="controls"> <div v-if="state.offer?.active && !state.p2Action" class="controls">
<div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div> <div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div>
<div v-if="myRole === 'P2'"> <div v-if="myRole === 'P2'">

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="g"> <div class="g">
<h3>G5 Cheap talk (chat previo no vinculante)</h3> <h3>G5 Cheap talk (chat previo no vinculante)</h3>
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/> <OfferControls v-if="myRole==='P1' && !state.offer?.active" :my-role="myRole" @propose="onPropose" @no-offer="onNoOffer"/>
<div v-if="state.offer?.active && !state.p2Action" class="controls"> <div v-if="state.offer?.active && !state.p2Action" class="controls">
<div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div> <div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div>
<div v-if="myRole === 'P2'"> <div v-if="myRole === 'P2'">

View File

@@ -1,38 +1,104 @@
<template> <template>
<div class="offer"> <div class="offer-card">
<div class="row"> <div class="offer-grid">
<label>Ofrezco:</label> <div class="group">
<input type="number" min="0" v-model.number="offerPavo" /> 🦃 <div class="group-title">Ofrezco</div>
<input type="number" min="0" v-model.number="offerElote" /> 🌽 <div class="tokens">
<div class="token-ctrl">
<span class="icon">🦃</span>
<div class="ctrl">
<button class="step" @click="dec('offerPavo')" aria-label="-1 pavo" tabindex="-1"></button>
<input type="number" min="0" :max="maxOfferPavo" v-model.number="offerPavo" />
<button class="step" @click="inc('offerPavo')" aria-label="+1 pavo" tabindex="-1"></button>
</div>
</div>
<div class="token-ctrl">
<span class="icon">🌽</span>
<div class="ctrl">
<button class="step" @click="dec('offerElote')" aria-label="-1 elote" tabindex="-1"></button>
<input type="number" min="0" :max="maxOfferElote" v-model.number="offerElote" />
<button class="step" @click="inc('offerElote')" aria-label="+1 elote" tabindex="-1"></button>
</div>
</div>
</div>
</div>
<div class="group">
<div class="group-title">A cambio</div>
<div class="tokens">
<div class="token-ctrl">
<span class="icon">🦃</span>
<div class="ctrl">
<button class="step" @click="dec('requestPavo')" aria-label="-1 pavo" tabindex="-1"></button>
<input type="number" min="0" :max="maxRequestPavo" v-model.number="requestPavo" />
<button class="step" @click="inc('requestPavo')" aria-label="+1 pavo" tabindex="-1"></button>
</div>
</div>
<div class="token-ctrl">
<span class="icon">🌽</span>
<div class="ctrl">
<button class="step" @click="dec('requestElote')" aria-label="-1 elote" tabindex="-1"></button>
<input type="number" min="0" :max="maxRequestElote" v-model.number="requestElote" />
<button class="step" @click="inc('requestElote')" aria-label="+1 elote" tabindex="-1"></button>
</div>
</div>
</div>
</div>
</div> </div>
<div class="row">
<label>A cambio:</label> <div class="actions">
<input type="number" min="0" v-model.number="requestPavo" /> 🦃
<input type="number" min="0" v-model.number="requestElote" /> 🌽
</div>
<div class="controls">
<button class="btn primary" @click="propose">Enviar oferta</button> <button class="btn primary" @click="propose">Enviar oferta</button>
<button class="btn" @click="noOffer" :disabled="disableNoOffer">No ofrecer</button> <button class="btn ghost" @click="noOffer" :disabled="disableNoOffer">No ofrecer</button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, computed, watch } from 'vue';
const props = defineProps<{ disableNoOffer?: boolean }>(); import { colyseusService } from '../../services/colyseus';
const props = defineProps<{ disableNoOffer?: boolean; myRole?: string }>();
const emit = defineEmits(['propose','no-offer']); const emit = defineEmits(['propose','no-offer']);
const offerPavo = ref(0); const offerPavo = ref(0);
const offerElote = ref(0); const offerElote = ref(0);
const requestPavo = ref(0); const requestPavo = ref(0);
const requestElote = ref(0); const requestElote = ref(0);
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 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));
function clampAll() {
offerPavo.value = Math.max(0, Math.min(offerPavo.value | 0, maxOfferPavo.value));
offerElote.value = Math.max(0, Math.min(offerElote.value | 0, maxOfferElote.value));
requestPavo.value = Math.max(0, Math.min(requestPavo.value | 0, maxRequestPavo.value));
requestElote.value = Math.max(0, Math.min(requestElote.value | 0, maxRequestElote.value));
}
watch([offerPavo, offerElote, requestPavo, requestElote, maxOfferPavo, maxOfferElote, maxRequestPavo, maxRequestElote], clampAll);
function inc(key: 'offerPavo'|'offerElote'|'requestPavo'|'requestElote') {
if (key === 'offerPavo') offerPavo.value = Math.min((offerPavo.value|0)+1, maxOfferPavo.value);
else if (key === 'offerElote') offerElote.value = Math.min((offerElote.value|0)+1, maxOfferElote.value);
else if (key === 'requestPavo') requestPavo.value = Math.min((requestPavo.value|0)+1, maxRequestPavo.value);
else requestElote.value = Math.min((requestElote.value|0)+1, maxRequestElote.value);
}
function dec(key: 'offerPavo'|'offerElote'|'requestPavo'|'requestElote') {
if (key === 'offerPavo') offerPavo.value = Math.max(0, offerPavo.value - 1);
else if (key === 'offerElote') offerElote.value = Math.max(0, offerElote.value - 1);
else if (key === 'requestPavo') requestPavo.value = Math.max(0, requestPavo.value - 1);
else requestElote.value = Math.max(0, requestElote.value - 1);
}
function propose() { function propose() {
// Always emit the proposal with current values // Always emit the proposal with current values
const payload = { const payload = {
offerPavo: Math.max(0, offerPavo.value|0), offerPavo: Math.max(0, Math.min(offerPavo.value|0, maxOfferPavo.value)),
offerElote: Math.max(0, offerElote.value|0), offerElote: Math.max(0, Math.min(offerElote.value|0, maxOfferElote.value)),
requestPavo: Math.max(0, requestPavo.value|0), requestPavo: Math.max(0, Math.min(requestPavo.value|0, maxRequestPavo.value)),
requestElote: Math.max(0, requestElote.value|0) requestElote: Math.max(0, Math.min(requestElote.value|0, maxRequestElote.value))
}; };
emit('propose', payload); emit('propose', payload);
@@ -54,12 +120,26 @@ function noOffer() {
</script> </script>
<style scoped> <style scoped>
.offer { background:#f9fafb; padding:10px; border-radius:8px; } .offer-card { margin-top:10px; }
.row { display:flex; align-items:center; gap:8px; margin-bottom:8px; } .offer-grid { display:grid; grid-template-columns: 1fr; gap:12px; }
label { width:70px; color:#555; } @media (min-width: 500px) { .offer-grid { grid-template-columns: 1fr 1fr; } }
input { width:80px; padding:6px; border:1px solid #ddd; border-radius:6px; }
.controls { display:flex; gap:8px; } .group { background:#f8fafc; border:1px solid #e5e9f0; border-radius:10px; padding:5px; }
.btn { padding:6px 10px; border:none; border-radius:6px; background:#e3f2fd; color:#1565c0; cursor:pointer; } .group-title { font-weight:700; font-size:14px; color:#334155; margin-bottom:8px; }
.btn:disabled { opacity:0.5; cursor:not-allowed; } .tokens { display:grid; grid-template-columns: 1fr; gap:5px; }
.btn.primary { background:#667eea; color:#fff; } .token-ctrl { display:flex; align-items:center; gap:10px; }
.icon { font-size: 18px; width: 20px; text-align:center; }
.ctrl { display:flex; align-items:center; gap:6px; background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:6px; }
.ctrl input { width: 50px; padding:6px; border:1px solid #e2e8f0; border-radius:8px; text-align:center; font-weight:600; }
.ctrl input::-webkit-outer-spin-button, .ctrl input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.ctrl input[type=number] { -moz-appearance: textfield; }
.step { width:28px; height:28px; border-radius:8px; border:1px solid #cbd5e1; background:#f1f5f9; cursor:pointer; line-height:1; display:flex; align-items:center; justify-content:center; }
.step:hover { background:#e2e8f0; }
.actions { display:flex; gap:10px; justify-content:flex-end; margin-top:12px; flex-wrap: wrap; }
.btn { padding:10px 14px; border:none; border-radius:10px; cursor:pointer; font-weight:700; }
.btn.primary { background:#667eea; color:#fff; box-shadow: 0 10px 20px rgba(102,126,234,0.35); }
.btn.primary:hover { filter: brightness(1.05); }
.btn.ghost { background:#eef2ff; color:#3949ab; border:1px solid #c7d2fe; }
.btn.ghost:disabled { opacity:0.5; cursor:not-allowed; }
</style> </style>