habits.andr33v.ru/app/components/VillageGrid.vue

298 lines
7.1 KiB
Vue

<template>
<div v-if="villageData && villageData.tiles" class="village-container">
<div :class="villageGridWrapperClass" :style="gridWrapperStyle">
<!-- Empty corner for alignment -->
<div class="empty-corner"></div>
<!-- Column Labels (A, B, C...) -->
<div class="col-labels">
<div class="col-label" v-for="colLabel in colLabels" :key="colLabel">{{ colLabel }}</div>
</div>
<!-- Row Labels (7, 6, 5...) -->
<div class="row-labels">
<div class="row-label" v-for="rowLabel in rowLabels" :key="rowLabel">{{ rowLabel }}</div>
</div>
<!-- The actual grid -->
<div class="village-grid" :style="gridStyle">
<div
v-for="tile in villageData.tiles"
:key="tile.id"
class="tile"
:class="tileClasses(tile)"
:style="{ 'grid-column': tile.x + 1, 'grid-row': gridHeight - tile.y }"
@click="$emit('tile-click', tile)"
>
<span class="tile-content">{{ getTileEmoji(tile) }}</span>
</div>
</div>
</div>
</div>
<div v-else class="loading">
Загрузка деревни...
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
// --- Props & Emits ---
const props = defineProps({
villageData: {
type: Object,
required: true,
},
isOnboarding: {
type: Boolean,
default: false,
},
});
const villageGridWrapperClass = computed(() => ({
'village-grid-wrapper': true,
'is-onboarding': props.isOnboarding,
}));
defineEmits(['tile-click']);
// --- Grid Dimensions ---
const gridWidth = computed(() => {
if (!props.villageData?.tiles || props.villageData.tiles.length === 0) return 5; // Default
return Math.max(...props.villageData.tiles.map(t => t.x)) + 1;
});
const gridHeight = computed(() => {
if (!props.villageData?.tiles || props.villageData.tiles.length === 0) return 7; // Default
return Math.max(...props.villageData.tiles.map(t => t.y)) + 1;
});
// --- Dynamic Styles ---
const gridWrapperStyle = computed(() => ({
'grid-template-columns': `20px repeat(${gridWidth.value}, var(--tile-size))`,
'grid-template-rows': `repeat(${gridHeight.value}, var(--tile-size)) 20px`,
}));
const gridStyle = computed(() => ({
'grid-template-columns': `repeat(${gridWidth.value}, var(--tile-size))`,
'grid-template-rows': `repeat(${gridHeight.value}, var(--tile-size))`,
}));
// --- Labels ---
const colLabels = computed(() => {
return Array.from({ length: gridWidth.value }, (_, i) => String.fromCharCode(65 + i));
});
const rowLabels = computed(() => {
return Array.from({ length: gridHeight.value }, (_, i) => gridHeight.value - i);
});
// --- Tile Display Logic ---
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',
};
};
</script>
<style scoped>
.village-container {
display: flex;
justify-content: center;
width: 100%;
padding: 0 10px;
margin-top: 20px;
--tile-size: clamp(40px, 10vw, 55px);
}
.village-grid-wrapper {
display: grid;
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);
overflow-x: auto;
overflow-y: hidden;
}
.empty-corner, .col-labels, .row-labels {
opacity: 0.6;
font-size: 0.8rem;
}
.col-labels {
grid-column: 2 / -1; /* Dynamic spanning */
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 / -1; /* Dynamic spanning */
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 / -1; /* Dynamic spanning */
grid-row: 1 / -1; /* Dynamic spanning */
display: grid;
gap: 8px; /* Increased gap */
}
.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);
}
.village-grid-wrapper.is-onboarding {
padding: 8px;
gap: 4px;
}
.village-grid-wrapper.is-onboarding .col-label,
.village-grid-wrapper.is-onboarding .row-label {
font-size: 0.7rem;
}
@media (max-width: 480px) {
.village-grid-wrapper {
gap: 4px;
padding: 8px;
}
.village-grid {
gap: 4px;
}
}
@media (max-width: 768px) {
.village-grid-wrapper {
width: 100%;
margin: 0;
box-sizing: border-box;
padding-left: 0;
padding-right: 0;
position: relative; /* Needed for pseudo-element */
}
.village-grid-wrapper::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 20px;
background: linear-gradient(to left, rgba(0,0,0,0.05), transparent);
pointer-events: none; /* Let clicks pass through */
opacity: 0;
transition: opacity 0.2s;
}
.village-grid-wrapper:hover::after {
opacity: 1;
}
.tile {
aspect-ratio: 1 / 1; /* Keep tiles square */
height: auto; /* Let aspect ratio control the height */
}
}
</style>