791 lines
21 KiB
Vue
791 lines
21 KiB
Vue
<template>
|
||
<div class="page-container village-page-layout">
|
||
<h1>Моя деревня</h1>
|
||
|
||
<div v-if="pending" class="loading">Загрузка вашей деревни...</div>
|
||
|
||
<div v-else-if="error" class="error-container">
|
||
<p v-if="error.statusCode === 401">Пожалуйста, войдите, чтобы увидеть свою деревню.</p>
|
||
<div v-else>
|
||
<p>Произошла ошибка при загрузке данных о деревне. Пожалуйста, попробуйте снова.</p>
|
||
<pre>{{ error }}</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="villageData">
|
||
<div class="village-container">
|
||
<div class="village-grid-wrapper"> <!-- This will be the main grid container with labels -->
|
||
<div class="empty-corner"></div> <!-- Top-left empty cell -->
|
||
|
||
<div class="col-labels">
|
||
<div class="col-label" v-for="colLabel in ['A', 'B', 'C', 'D', 'E']" :key="colLabel">{{ colLabel }}</div>
|
||
</div>
|
||
|
||
<div class="row-labels">
|
||
<div class="row-label" v-for="rowLabel in ['7', '6', '5', '4', '3', '2', '1']" :key="rowLabel">{{ rowLabel }}</div>
|
||
</div>
|
||
|
||
<div class="village-grid">
|
||
<div
|
||
v-for="tile in villageData.tiles"
|
||
:key="tile.id"
|
||
class="tile"
|
||
:class="[tileClasses(tile), { selected: selectedTile && selectedTile.id === tile.id }]"
|
||
:style="{ 'grid-column': tile.x + 1, 'grid-row': tile.y + 1 }"
|
||
@click="selectTile(tile)"
|
||
>
|
||
<span class="tile-content">{{ getTileEmoji(tile) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tile Info Overlay -->
|
||
<div v-if="selectedTile" class="tile-overlay-backdrop" @click="selectedTile = null">
|
||
<div class="tile-overlay-panel" @click.stop>
|
||
|
||
<h2>{{ getTileTitle(selectedTile) }}</h2>
|
||
<p class="tile-description">{{ getTileDescription(selectedTile) }}</p>
|
||
|
||
<div v-if="selectedTile.availableActions && selectedTile.availableActions.length > 0" class="actions-container">
|
||
<h3 class="actions-header">Что здесь можно сделать?</h3>
|
||
<div class="actions-list">
|
||
<!-- Build Actions -->
|
||
<div v-if="selectedTile.availableActions.some(a => a.type === 'BUILD')" class="build-section">
|
||
<div class="build-card-grid">
|
||
<div
|
||
v-for="action in selectedTile.availableActions.filter(a => a.type === 'BUILD')"
|
||
:key="action.buildingType"
|
||
class="building-card"
|
||
:class="{ disabled: !action.isEnabled }"
|
||
>
|
||
<div class="building-icon">{{ getBuildingEmoji(action.buildingType) }}</div>
|
||
<h5>{{ getBuildingName(action.buildingType) }}</h5>
|
||
<p class="building-description">{{ getBuildingDescription(action.buildingType) }}</p>
|
||
<div class="building-footer">
|
||
<button :disabled="!action.isEnabled || isSubmitting" @click="handleActionClick(action)" class="btn btn-primary btn-sm btn-full-width">
|
||
{{ getActionLabel(action) }}
|
||
</button>
|
||
</div>
|
||
<div v-if="!action.isEnabled" class="disabled-overlay">
|
||
<span>{{ getDisabledReasonText(action) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button @click="selectedTile = null" class="btn btn-secondary close-overlay-button">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bottom-content">
|
||
<!-- Admin Panel -->
|
||
<div v-if="villageData?.user?.id === 1" class="admin-panel">
|
||
<h3>Admin Tools</h3>
|
||
<button @click="handleResetVillage" :disabled="isSubmittingAdminAction">Reset Village</button>
|
||
<button @click="handleTriggerTick" :disabled="isSubmittingAdminAction">Trigger Next Tick</button>
|
||
<button @click="handleAddCoins" :disabled="isSubmittingAdminAction">Add 1000 Coins</button>
|
||
</div>
|
||
|
||
<!-- Event Log -->
|
||
<div v-if="villageEvents?.length" class="event-log-container">
|
||
<h2>Журнал событий</h2>
|
||
<div class="event-list">
|
||
<div v-for="event in villageEvents" :key="event.id" class="event-card">
|
||
<div class="event-card-header">
|
||
<span class="event-date">{{ new Date(event.createdAt).toLocaleString() }}</span>
|
||
<div class="event-rewards">
|
||
<span v-if="event.coins" class="event-reward-tag coins">{{ event.coins }} 💰</span>
|
||
<span v-if="event.exp" class="event-reward-tag exp">{{ event.exp }} ✨</span>
|
||
</div>
|
||
</div>
|
||
<p class="event-message">{{ formatMessageCoordinates(event.message) }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
|
||
const { formatCoordinates, formatMessageCoordinates } = useVillageHelpers();
|
||
const { user, isAuthenticated, logout, updateUser } = useAuth(); // Destructure updateUser
|
||
|
||
const { data: villageData, pending, error, refresh: refreshVillageData } = await useFetch('/api/village', {
|
||
lazy: true,
|
||
server: false, // Ensure this runs on the client-side
|
||
});
|
||
|
||
const { data: villageEvents, refresh: refreshEvents } = await useFetch('/api/village/events', {
|
||
lazy: true,
|
||
server: false,
|
||
});
|
||
|
||
const selectedTile = ref(null);
|
||
|
||
const getTileEmoji = (tile) => {
|
||
if (tile.terrainState === 'CLEARING') return '⏳';
|
||
if (tile.object) {
|
||
switch (tile.object.type) {
|
||
case 'HOUSE': return '🏠';
|
||
case 'FIELD': return '🌱';
|
||
case 'LUMBERJACK': return '🪓';
|
||
case 'QUARRY': return '⛏️';
|
||
case 'WELL': return '💧';
|
||
default: return '❓';
|
||
}
|
||
}
|
||
switch (tile.terrainType) {
|
||
case 'BLOCKED_TREE': return '🌳';
|
||
case 'BLOCKED_STONE': return '🪨';
|
||
case 'EMPTY': return '';
|
||
default: return '❓';
|
||
}
|
||
};
|
||
|
||
const tileClasses = (tile) => {
|
||
return {
|
||
'tile-blocked': tile.terrainType === 'BLOCKED_TREE' || tile.terrainType === 'BLOCKED_STONE',
|
||
'tile-object': !!tile.object,
|
||
'tile-empty': tile.terrainType === 'EMPTY' && !tile.object,
|
||
'tile-clearing': tile.terrainState === 'CLEARING', // Add this line
|
||
};
|
||
};
|
||
|
||
const selectTile = (tile) => {
|
||
selectedTile.value = tile;
|
||
};
|
||
|
||
const getActionLabel = (action) => {
|
||
if (action.type === 'BUILD') {
|
||
return `${action.cost} монет`; // Return cost instead of "Построить"
|
||
}
|
||
return action.type;
|
||
};
|
||
|
||
const getBuildingName = (buildingType) => {
|
||
const buildingNameMap = {
|
||
'HOUSE': 'Дом',
|
||
'FIELD': 'Поле',
|
||
'LUMBERJACK': 'Домик лесоруба',
|
||
'QUARRY': 'Каменоломня',
|
||
'WELL': 'Колодец',
|
||
};
|
||
return buildingNameMap[buildingType] || buildingType;
|
||
};
|
||
|
||
const getBuildingEmoji = (buildingType) => {
|
||
const emojiMap = {
|
||
'HOUSE': '🏠',
|
||
'FIELD': '🌱',
|
||
'LUMBERJACK': '🪓',
|
||
'QUARRY': '⛏️',
|
||
'WELL': '💧',
|
||
};
|
||
return emojiMap[buildingType] || '❓';
|
||
};
|
||
|
||
|
||
const getTileTitle = (tile) => {
|
||
if (tile.object) {
|
||
const buildingMap = {
|
||
'HOUSE': 'Дом',
|
||
'FIELD': 'Поле',
|
||
'LUMBERJACK': 'Домик лесоруба',
|
||
'QUARRY': 'Каменоломня',
|
||
'WELL': 'Колодец',
|
||
};
|
||
return `${buildingMap[tile.object.type] || 'Неизвестное строение'} ${formatCoordinates(tile.x, tile.y)}`;
|
||
}
|
||
const terrainMap = {
|
||
'BLOCKED_TREE': 'Лесной участок',
|
||
'BLOCKED_STONE': 'Каменистый участок',
|
||
'EMPTY': 'Пустырь',
|
||
};
|
||
return `${terrainMap[tile.terrainType] || 'Неизвестная земля'} ${formatCoordinates(tile.x, tile.y)}`;
|
||
};
|
||
|
||
const getTileDescription = (tile) => {
|
||
if (tile.terrainState === 'CLEARING') return 'Идет расчистка...';
|
||
if (tile.object) return `Здесь стоит ${tile.object.type}.`;
|
||
|
||
const descriptionMap = {
|
||
'BLOCKED_TREE': 'Густые деревья, которые можно расчистить с помощью лесоруба.',
|
||
'BLOCKED_STONE': 'Каменные завалы, которые можно убрать с помощью каменотеса.',
|
||
'EMPTY': 'Свободное место, готовое к застройке.',
|
||
};
|
||
return descriptionMap[tile.terrainType] || 'Это место выглядит странно.';
|
||
};
|
||
|
||
const getBuildingDescription = (buildingType) => {
|
||
const descriptions = {
|
||
'HOUSE': 'Увеличивает лимит рабочих на 1. Рабочие нужны для производственных зданий.',
|
||
'FIELD': 'Ежедневно приносит опыт. Производство можно увеличить, построив рядом колодец.',
|
||
'LUMBERJACK': 'Позволяет вашим рабочим расчищать участки с деревьями.',
|
||
'QUARRY': 'Позволяет вашим рабочим разбирать каменные завалы.',
|
||
'WELL': 'Увеличивает производство опыта на соседних полях.',
|
||
};
|
||
return descriptions[buildingType] || '';
|
||
};
|
||
|
||
const getDisabledReasonText = (action) => {
|
||
const reasons = {
|
||
'Not enough coins': 'Не хватает монет.',
|
||
'Not enough workers': 'Не хватает свободных рабочих (постройте больше домов).',
|
||
'Requires Lumberjack': 'Нужен домик лесоруба, чтобы убирать деревья.',
|
||
'Requires Quarry': 'Нужна каменоломня, чтобы убирать камни.',
|
||
};
|
||
return reasons[action.disabledReason] || action.disabledReason;
|
||
};
|
||
|
||
const isSubmitting = ref(false);
|
||
|
||
const handleActionClick = async (action) => {
|
||
if (isSubmitting.value) return;
|
||
|
||
isSubmitting.value = true;
|
||
try {
|
||
const response = await useFetch('/api/village/action', {
|
||
method: 'POST',
|
||
body: {
|
||
tileId: selectedTile.value.id,
|
||
actionType: action.type,
|
||
payload: {
|
||
...(action.type === 'BUILD' && { buildingType: action.buildingType }),
|
||
},
|
||
},
|
||
});
|
||
|
||
if (response.error.value) {
|
||
alert(response.error.value.data?.statusMessage || 'An unknown error occurred.');
|
||
} else {
|
||
villageData.value = response.data.value;
|
||
updateUser(response.data.value.user); // Update global user state
|
||
selectedTile.value = null;
|
||
await refreshEvents(); // Refresh the event log
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to perform action:', e);
|
||
alert('An unexpected error occurred. Please check the console.');
|
||
} finally {
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
const isSubmittingAdminAction = ref(false);
|
||
|
||
async function handleAdminAction(url: string) {
|
||
if (isSubmittingAdminAction.value) return;
|
||
isSubmittingAdminAction.value = true;
|
||
|
||
try {
|
||
// 1. Perform the requested admin action (e.g., reset, trigger tick)
|
||
const { error: actionError } = await useFetch(url, { method: 'POST' });
|
||
if (actionError.value) {
|
||
// If the action itself fails, throw to stop execution
|
||
throw actionError.value;
|
||
}
|
||
|
||
// 2. Refresh the main village data. This runs the core game logic
|
||
// on the backend and gets the updated state.
|
||
await refreshVillageData();
|
||
|
||
// 3. The `villageData` ref is now updated. If it contains a user object,
|
||
// we sync it with the global auth state to update the header.
|
||
if (villageData.value?.user) {
|
||
updateUser(villageData.value.user);
|
||
}
|
||
|
||
// 4. Refresh the event log to show any new events created by the action.
|
||
await refreshEvents();
|
||
|
||
} catch (e: any) {
|
||
console.error(`Failed to perform admin action at ${url}:`, e);
|
||
// Use the error's data object if it exists for a more specific message
|
||
alert(e.data?.statusMessage || e.message || 'An unexpected error occurred during the admin action.');
|
||
} finally {
|
||
isSubmittingAdminAction.value = false;
|
||
}
|
||
}
|
||
|
||
const handleResetVillage = () => handleAdminAction('/api/admin/village/reset');
|
||
const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-tick');
|
||
const handleAddCoins = () => handleAdminAction('/api/admin/user/add-coins');
|
||
</script>
|
||
|
||
<style scoped>
|
||
.village-page-layout {
|
||
--tile-size: clamp(55px, 12vw, 70px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.loading, .error-container {
|
||
margin-top: 50px;
|
||
font-size: 1.2em;
|
||
color: #555;
|
||
text-align: center;
|
||
}
|
||
|
||
.village-container {
|
||
max-width: fit-content; /* Changed from width: 100%; display: flex; justify-content: center; */
|
||
margin: 0 auto; /* Centered horizontally */
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* --- New Demo-Inspired Tile Styles --- */
|
||
.tile {
|
||
width: var(--tile-size);
|
||
height: var(--tile-size);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
position: relative; /* For selection pseudo-elements */
|
||
}
|
||
|
||
.tile.tile-empty {
|
||
background-color: rgba(0,0,0,0.02);
|
||
border: 2px dashed var(--border-color);
|
||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.03);
|
||
}
|
||
.tile.tile-empty:hover {
|
||
background-color: rgba(0,0,0,0.04);
|
||
border-color: var(--primary-color-light);
|
||
}
|
||
|
||
.tile.tile-blocked {
|
||
background-color: #EBE5D7; /* A soft, earthy tone */
|
||
border: 1px solid #DCD5C6;
|
||
}
|
||
.tile.tile-blocked:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.tile.tile-object {
|
||
background-color: #c8ffcf;
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.07);
|
||
border: 1px solid #39af50;
|
||
}
|
||
.tile.tile-object:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.tile.tile-clearing {
|
||
background-color: #ffe0b2; /* A soft orange/yellow for "in progress" */
|
||
border: 1px solid #ffcc80;
|
||
}
|
||
|
||
.tile.selected {
|
||
box-shadow: 0 0 0 3px var(--primary-color);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.tile-content {
|
||
font-size: calc(var(--tile-size) * 0.5); /* Make emoji scale with tile */
|
||
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
}
|
||
|
||
.tile.tile-object .tile-content {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
|
||
/* Styles for the grid with labels */
|
||
.village-grid-wrapper {
|
||
display: grid;
|
||
grid-template-columns: 20px repeat(5, var(--tile-size));
|
||
grid-template-rows: repeat(7, var(--tile-size)) 20px;
|
||
gap: 8px; /* Increased gap */
|
||
border-radius: 16px; /* Rounded wrapper */
|
||
background-color: var(--container-bg-color);
|
||
padding: 12px; /* Increased padding */
|
||
width: fit-content;
|
||
margin: 0 auto;
|
||
box-shadow: 0 8px 30px rgba(0,0,0,0.06);
|
||
/* Removed overflow-x: auto; and overflow-y: hidden; */
|
||
}
|
||
|
||
.empty-corner, .col-labels, .row-labels {
|
||
opacity: 0.6;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.empty-corner {
|
||
grid-column: 1;
|
||
grid-row: 8;
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.col-labels {
|
||
grid-column: 2 / span 5;
|
||
grid-row: 8;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
height: 20px;
|
||
}
|
||
|
||
.col-label {
|
||
width: var(--tile-size);
|
||
text-align: center;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.row-labels {
|
||
grid-column: 1;
|
||
grid-row: 1 / span 7;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
width: 20px;
|
||
}
|
||
|
||
.row-label {
|
||
height: var(--tile-size);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.village-grid {
|
||
grid-column: 2 / span 5;
|
||
grid-row: 1 / span 7;
|
||
display: grid;
|
||
grid-template-columns: repeat(5, var(--tile-size));
|
||
grid-template-rows: repeat(7, var(--tile-size));
|
||
gap: 8px; /* Increased gap */
|
||
}
|
||
|
||
|
||
/* Overlay and other styles */
|
||
|
||
.tile-overlay-backdrop {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: flex-end;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.tile-overlay-panel {
|
||
background-color: var(--background-color);
|
||
width: 100%;
|
||
max-width: 500px;
|
||
padding: 24px;
|
||
border-top-left-radius: 15px;
|
||
border-top-right-radius: 15px;
|
||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.tile-overlay-backdrop {
|
||
align-items: center;
|
||
}
|
||
.tile-overlay-panel {
|
||
border-radius: 15px;
|
||
}
|
||
}
|
||
|
||
.tile-overlay-panel h2 {
|
||
text-align: center;
|
||
}
|
||
|
||
.tile-description {
|
||
text-align: center;
|
||
margin: -10px;
|
||
color: var(--text-color-light);
|
||
font-style: italic;
|
||
}
|
||
|
||
.actions-container {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.actions-header {
|
||
text-align: center;
|
||
}
|
||
|
||
.build-section {
|
||
padding-top: 15px;
|
||
border-top: 1px solid var(--border-color);
|
||
}
|
||
|
||
.building-description {
|
||
font-size: 0.85em;
|
||
color: var(--text-color-light);
|
||
margin-top: 5px;
|
||
text-align: center;
|
||
}
|
||
|
||
.close-overlay-button {
|
||
width: 100%;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.bottom-content {
|
||
width: 100%;
|
||
max-width: 800px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 24px;
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.admin-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 15px;
|
||
border: 2px dashed var(--danger-color);
|
||
border-radius: 10px;
|
||
width: 100%;
|
||
max-width: 350px;
|
||
}
|
||
|
||
.admin-panel h3 {
|
||
margin: 0 0 10px 0;
|
||
color: var(--danger-color);
|
||
}
|
||
|
||
.admin-panel button {
|
||
width: 100%;
|
||
padding: 8px;
|
||
background-color: var(--danger-color);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.admin-panel button:disabled {
|
||
background-color: #e9ecef;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
|
||
.event-log-container {
|
||
width: 100%;
|
||
max-width: 800px;
|
||
}
|
||
|
||
.event-log-container h2 {
|
||
text-align: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.event-list {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
padding-right: 10px; /* For scrollbar spacing */
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.event-card {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
border: 1px solid var(--border-color);
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.event-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.event-date {
|
||
font-size: 0.8rem;
|
||
color: var(--text-color-light);
|
||
}
|
||
|
||
.event-rewards {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.event-reward-tag {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
padding: 2px 6px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.event-reward-tag.coins {
|
||
background-color: var(--warning-color-light);
|
||
color: var(--warning-color-dark);
|
||
}
|
||
|
||
.event-reward-tag.exp {
|
||
background-color: var(--info-color-light);
|
||
color: var(--info-color-dark);
|
||
}
|
||
|
||
.event-message {
|
||
font-size: 0.95rem;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.build-card-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.building-card {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 140px;
|
||
padding: 16px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 8px;
|
||
background-color: #fff;
|
||
text-align: center;
|
||
transition: box-shadow 0.2s;
|
||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||
}
|
||
|
||
.building-card:not(.disabled):hover {
|
||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||
}
|
||
|
||
.building-card.disabled {
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
.building-icon {
|
||
font-size: 2.5em;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.building-card h5 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 1.05em;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.building-card .building-description {
|
||
font-size: 0.8rem;
|
||
line-height: 1.5;
|
||
color: var(--text-color-light);
|
||
flex-grow: 1;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.building-footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.building-footer .btn-full-width {
|
||
width: 100%;
|
||
}
|
||
|
||
.building-footer .cost {
|
||
font-weight: 600;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.disabled-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(249, 250, 251, 0.85);
|
||
color: var(--danger-color);
|
||
font-weight: 600;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
text-align: center;
|
||
padding: 10px;
|
||
border-radius: 8px;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.disabled-overlay span {
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.village-grid-wrapper {
|
||
gap: 4px;
|
||
padding: 8px;
|
||
}
|
||
.village-grid {
|
||
gap: 4px;
|
||
}
|
||
}
|
||
|
||
/* --- Responsive styles for mobile --- */
|
||
@media (max-width: 768px) {
|
||
/* No changes to village-grid-wrapper or village-grid for 'fr' units,
|
||
as the --tile-size clamping already handles mobile scaling without fr.
|
||
Removed margin/padding adjustments to keep it centered and scaled.
|
||
Removed position: relative and ::after pseudo-element for scroll shadow.
|
||
*/
|
||
|
||
.bottom-content {
|
||
width: 100%;
|
||
padding: 0 16px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.tile-overlay-panel {
|
||
padding: 16px; /* Reduce padding on mobile */
|
||
margin-bottom: 60px;
|
||
gap: 12px;
|
||
max-width: 95%; /* Make it almost full width */
|
||
border-top-left-radius: 15px;
|
||
border-top-right-radius: 15px;
|
||
}
|
||
|
||
.build-card-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.building-card {
|
||
width: 130px; /* A fixed width that allows for 2 cards per row on most phones */
|
||
}
|
||
}
|
||
</style> |