habits.andr33v.ru/app/pages/village.vue

653 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="village-page">
<h1>My Village</h1>
<div v-if="pending" class="loading">Loading your village...</div>
<div v-else-if="error" class="error-container">
<p v-if="error.statusCode === 401">Please log in to view your village.</p>
<p v-else>An error occurred while fetching your village data. Please try again.</p>
</div>
<div v-else-if="villageData" class="village-container">
<div class="village-grid-wrapper">
<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 }]"
@click="selectTile(tile)"
>
<span class="tile-content">{{ getTileEmoji(tile) }}</span>
</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">
<div class="cost">
{{ action.cost }} монет
</div>
<button :disabled="!action.isEnabled || isSubmitting" @click="handleActionClick(action)">
{{ 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="close-overlay-button">Закрыть</button>
</div>
</div>
</div>
<!-- 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="handleCompleteClearing" :disabled="isSubmittingAdminAction">Complete All Clearing</button>
<button @click="handleTriggerTick" :disabled="isSubmittingAdminAction">Trigger Next Tick</button>
</div>
<!-- Event Log -->
<div v-if="villageEvents?.length" class="event-log-container">
<h4>Activity Log</h4>
<table class="event-log-table">
<thead>
<tr>
<th>Date</th>
<th>Event</th>
<th>Coins</th>
<th>EXP</th>
</tr>
</thead>
<tbody>
<tr v-for="event in villageEvents" :key="event.id">
<td>{{ new Date(event.createdAt).toLocaleString() }}</td>
<td>{{ event.message }}</td>
<td>{{ event.coins }}</td>
<td>{{ event.exp }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuth } from '~/composables/useAuth'; // Import useAuth
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,
};
};
const selectTile = (tile) => {
selectedTile.value = tile;
};
const getActionLabel = (action) => {
if (action.type === 'BUILD') {
return `Построить`;
}
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] || 'Неизвестное строение'} (${tile.x}, ${tile.y})`;
}
const terrainMap = {
'BLOCKED_TREE': 'Лесной участок',
'BLOCKED_STONE': 'Каменистый участок',
'EMPTY': 'Пустырь',
};
return `${terrainMap[tile.terrainType] || 'Неизвестная земля'} (${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) {
if (isSubmittingAdminAction.value) return;
isSubmittingAdminAction.value = true;
try {
const { error } = await useFetch(url, { method: 'POST' });
if (error.value) {
alert(error.value.data?.statusMessage || 'An admin action failed.');
} else {
await Promise.all([refreshVillageData(), refreshEvents()]);
}
} catch (e) {
console.error('Failed to perform admin action:', e);
alert('An unexpected error occurred.');
} finally {
isSubmittingAdminAction.value = false;
}
}
const handleResetVillage = () => handleAdminAction('/api/admin/village/reset');
const handleCompleteClearing = () => handleAdminAction('/api/admin/village/complete-clearing');
const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-tick');
</script>
<style scoped>
.village-page {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
font-family: sans-serif;
min-height: calc(100vh - 120px);
box-sizing: border-box;
}
.loading, .error-container {
margin-top: 50px;
font-size: 1.2em;
color: #555;
}
.village-container {
display: flex;
justify-content: center;
width: 100%;
max-width: 350px;
margin-top: 20px;
}
.village-grid-wrapper {
overflow-x: auto;
}
.village-grid {
display: grid;
grid-template-columns: repeat(5, 60px);
grid-template-rows: repeat(7, 60px);
gap: 4px;
border: 2px solid #333;
padding: 4px;
background-color: #f0f0f0;
width: fit-content;
margin: 0 auto;
}
.tile {
width: 60px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #ccc;
background-color: #fff; /* Default white for empty */
cursor: pointer;
transition: background-color 0.2s;
}
.tile.tile-blocked {
background-color: #e0e0e0; /* Light gray for blocked (tree/stone) */
}
.tile.tile-object {
background-color: #e6ffe6; /* Light green for tiles with user objects */
}
.tile:hover {
background-color: #ffffe0; /* Light yellow for hover */
border: 1px solid #ffcc00; /* Subtle yellow border */
}
.tile.selected {
border: 2px solid #007bff;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
}
.tile-content {
font-size: 2em;
}
.tile-overlay-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-end;
z-index: 1000;
}
.tile-overlay-panel {
background-color: #fff;
width: 100%;
max-width: 500px;
padding: 20px;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
transform: translateY(0);
transition: transform 0.3s ease-out;
display: flex;
flex-direction: column;
gap: 15px;
}
@media (min-width: 768px) {
.tile-overlay-backdrop {
align-items: center;
}
.tile-overlay-panel {
border-radius: 15px;
max-height: 80vh;
}
}
.tile-overlay-panel h2 {
margin-top: 0;
text-align: center;
color: #333;
}
.tile-description {
text-align: center;
margin-top: -10px;
color: #666;
font-style: italic;
}
.actions-container {
margin-top: 15px;
}
.actions-header {
text-align: center;
font-size: 1.2em;
color: #444;
margin-bottom: 15px;
}
.actions-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.action-item {
margin-bottom: 10px;
}
.action-item button {
width: 100%;
padding: 10px 15px;
border: 1px solid #007bff;
background-color: #007bff;
color: white;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.2s, opacity 0.2s;
}
.action-item button:hover:not(:disabled) {
background-color: #0056b3;
}
.action-item button:disabled {
background-color: #e9ecef;
color: #6c757d;
cursor: not-allowed;
border-color: #e9ecef;
}
.build-section {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.build-section h4 {
text-align: center;
color: #555;
margin-bottom: 10px;
}
.building-description {
font-size: 0.85em;
color: #555;
margin-top: 5px;
text-align: center;
}
.disabled-reason {
font-size: 0.8em;
color: #dc3545;
margin-top: 5px;
text-align: center;
}
.close-overlay-button {
width: 100%;
padding: 10px 15px;
margin-top: 15px;
background-color: #6c757d;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.close-overlay-button:hover {
background-color: #5a6268;
}
.admin-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 20px;
padding: 15px;
border: 2px dashed #dc3545;
border-radius: 10px;
max-width: 350px;
width: 100%;
}
.admin-panel h3 {
margin: 0 0 10px 0;
color: #dc3545;
}
.admin-panel button {
width: 100%;
padding: 8px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.admin-panel button:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}
.event-log-container {
margin-top: 20px;
width: 100%;
max-width: 350px;
}
.event-log-container h4 {
text-align: center;
margin-bottom: 10px;
}
.event-log-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8em;
}
.event-log-table th, .event-log-table td {
border: 1px solid #ccc;
padding: 6px;
text-align: left;
}
.event-log-table th {
background-color: #f0f0f0;
}
/* New Build Card Styles */
.build-card-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.building-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 130px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
text-align: center;
transition: box-shadow 0.2s;
}
.building-card:not(.disabled):hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.building-card.disabled {
background-color: #e9ecef;
}
.building-icon {
font-size: 2em;
margin-bottom: 5px;
}
.building-card h5 {
margin: 0 0 5px 0;
font-size: 1em;
color: #333;
}
.building-card .building-description {
font-size: 0.75em;
color: #666;
flex-grow: 1; /* Pushes footer down */
margin-bottom: 10px;
}
.building-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-top: auto; /* Pushes footer to bottom */
}
.building-footer .cost {
font-weight: bold;
font-size: 0.9em;
}
.building-footer button {
padding: 5px 10px;
font-size: 0.9em;
}
.disabled-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(233, 236, 239, 0.8);
color: #dc3545;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 5px;
border-radius: 8px;
cursor: not-allowed;
}
.disabled-overlay span {
font-size: 0.9em;
}
</style>