mejoras UI OfferControls
This commit is contained in:
@@ -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; }
|
||||||
|
|||||||
@@ -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'">
|
||||||
|
|||||||
@@ -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'">
|
||||||
|
|||||||
@@ -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'">
|
||||||
|
|||||||
@@ -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'">
|
||||||
|
|||||||
@@ -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'">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user