добавлен админ тул

This commit is contained in:
Alexander Andreev 2026-01-04 17:31:43 +03:00
parent 4b83e8339f
commit 365d04b678
5 changed files with 184 additions and 47 deletions

View File

@ -33,7 +33,7 @@ To run the development server with hot-reloading:
```bash
npm run dev
```
Не запускай npm run dev сам. Скажи пользователю, что бы сделал это сам.
Никогда НЕ запускай npm run dev сам. Скажи пользователю, что бы сделал это сам.
### Building for Production
To build the application for production:

View File

@ -47,13 +47,20 @@
</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>
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
const { data: villageData, pending, error } = await useFetch('/api/village', {
const { data: villageData, pending, error, refresh: refreshVillageData } = await useFetch('/api/village', {
lazy: true,
server: false, // Ensure this runs on the client-side
});
@ -123,6 +130,30 @@ const handleActionClick = async (action) => {
isSubmitting.value = false;
}
};
const isSubmittingAdminAction = ref(false);
async function handleAdminAction(url: string) {
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 refreshVillageData();
}
} 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');
</script>
<style scoped>
@ -291,4 +322,37 @@ const handleActionClick = async (action) => {
.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;
}
</style>

View File

@ -0,0 +1,51 @@
// server/api/admin/village/complete-clearing.post.ts
import { getUserIdFromSession } from '../../../utils/auth';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
const userId = await getUserIdFromSession(event);
// Simple admin check
if (userId !== 1) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
}
const village = await prisma.village.findUniqueOrThrow({ where: { userId } });
const tilesToComplete = await prisma.villageTile.findMany({
where: {
villageId: village.id,
terrainState: 'CLEARING',
},
});
if (tilesToComplete.length === 0) {
return { success: true, message: 'No clearing tasks to complete.' };
}
await prisma.$transaction([
// Complete the tiles
prisma.villageTile.updateMany({
where: {
id: { in: tilesToComplete.map(t => t.id) },
},
data: {
terrainState: 'IDLE',
terrainType: 'EMPTY',
clearingStartedAt: null,
},
}),
// Give the user the rewards (1 coin and 1 exp per tile)
prisma.user.update({
where: { id: userId },
data: {
coins: { increment: tilesToComplete.length },
exp: { increment: tilesToComplete.length },
},
}),
]);
return { success: true, message: `Completed ${tilesToComplete.length} clearing tasks.` };
});

View File

@ -0,0 +1,33 @@
// server/api/admin/village/reset.post.ts
import { getUserIdFromSession } from '../../../utils/auth';
import { generateVillageForUser } from '../../../services/villageService';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
const userId = await getUserIdFromSession(event);
// Simple admin check
if (userId !== 1) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
}
const user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
const village = await prisma.village.findUnique({ where: { userId } });
if (village) {
await prisma.$transaction([
// Note: Order matters due to foreign key constraints.
// Delete objects first, then tiles.
prisma.villageObject.deleteMany({ where: { villageId: village.id } }),
prisma.villageTile.deleteMany({ where: { villageId: village.id } }),
prisma.village.delete({ where: { id: village.id } }),
]);
}
// Generate a fresh village
await generateVillageForUser(user);
return { success: true, message: 'Village has been reset.' };
});

View File

@ -166,58 +166,43 @@ export async function getVillageState(userId: number): Promise<FullVillage> {
villageSnapshot = await prisma.village.findUnique({ where: { userId }, include: { user: true, tiles: { include: { object: true } }, objects: { include: { tile: true } } } })!;
// --- Step 6: Auto-start Terrain Cleaning ---
const housesCount = villageSnapshot.objects.filter(o => o.type === 'HOUSE').length;
const producingCount = villageSnapshot.objects.filter(o => PRODUCING_BUILDINGS.includes(o.type)).length;
const freeWorkers = housesCount - producingCount;
const lumberjackCount = villageSnapshot.objects.filter(o => o.type === 'LUMBERJACK').length;
const quarryCount = villageSnapshot.objects.filter(o => o.type === 'QUARRY').length;
if (producingCount <= housesCount) {
const manhattanDistance = (p1: {x: number, y: number}, p2: {x: number, y: number}) => Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
const getDirectionalPriority = (worker: VillageTile, target: VillageTile): number => {
const dy = target.y - worker.y;
const dx = target.x - worker.x;
if (dy < 0) return 1; // Up
if (dx < 0) return 2; // Left
if (dx > 0) return 3; // Right
if (dy > 0) return 4; // Down
return 5;
};
const clearingTreesCount = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_TREE' && t.terrainState === 'CLEARING').length;
const clearingStonesCount = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_STONE' && t.terrainState === 'CLEARING').length;
const assignTasks = (workers: (VillageObject & { tile: VillageTile })[], targets: VillageTile[], newlyTargeted: Set<number>) => {
workers.forEach(worker => {
const potentialTargets = targets
.filter(t => !newlyTargeted.has(t.id))
.map(target => ({ target, distance: manhattanDistance(worker.tile, target) }))
.sort((a, b) => a.distance - b.distance);
const freeLumberjacks = lumberjackCount - clearingTreesCount;
const freeQuarries = quarryCount - clearingStonesCount;
if (!potentialTargets.length) return;
const tileIdsToClear = new Set<number>();
const minDistance = potentialTargets[0].distance;
const tiedTargets = potentialTargets.filter(t => t.distance === minDistance).map(t => t.target);
tiedTargets.sort((a, b) => getDirectionalPriority(worker.tile, a) - getDirectionalPriority(worker.tile, b));
const bestTarget = tiedTargets[0];
if (bestTarget) newlyTargeted.add(bestTarget.id);
});
};
const lumberjacks = villageSnapshot.objects.filter(obj => obj.type === 'LUMBERJACK') as (VillageObject & { tile: VillageTile })[];
const quarries = villageSnapshot.objects.filter(obj => obj.type === 'QUARRY') as (VillageObject & { tile: VillageTile })[];
if (freeLumberjacks > 0) {
const idleTrees = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_TREE' && t.terrainState === 'IDLE');
const idleStones = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_STONE' && t.terrainState === 'IDLE');
const tileIdsToClear = new Set<number>();
assignTasks(lumberjacks, idleTrees, tileIdsToClear);
assignTasks(quarries, idleStones, tileIdsToClear);
if (tileIdsToClear.size > 0) {
await prisma.villageTile.updateMany({
where: { id: { in: Array.from(tileIdsToClear) } },
data: { terrainState: 'CLEARING', clearingStartedAt: now },
});
if (idleTrees.length > 0) {
// For simplicity, just take the first N available trees. A more complex distance-based heuristic could go here.
idleTrees.slice(0, freeLumberjacks).forEach(t => tileIdsToClear.add(t.id));
}
}
if (freeQuarries > 0) {
const idleStones = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_STONE' && t.terrainState === 'IDLE');
if (idleStones.length > 0) {
// For simplicity, just take the first N available stones.
idleStones.slice(0, freeQuarries).forEach(t => tileIdsToClear.add(t.id));
}
}
if (tileIdsToClear.size > 0) {
await prisma.villageTile.updateMany({
where: { id: { in: Array.from(tileIdsToClear) } },
data: { terrainState: 'CLEARING', clearingStartedAt: now },
});
// Refetch state after starting new clearings
villageSnapshot = await prisma.village.findUnique({ where: { userId }, include: { user: true, tiles: { include: { object: true } }, objects: { include: { tile: true } } } })!;
}
// --- Step 7: Final Fetch & Action Enrichment ---
const finalVillageState = await prisma.village.findUnique({
where: { userId },
@ -236,6 +221,10 @@ export async function getVillageState(userId: number): Promise<FullVillage> {
const { user } = finalVillageState;
const hasLumberjack = finalVillageState.objects.some(o => o.type === 'LUMBERJACK');
const hasQuarry = finalVillageState.objects.some(o => o.type === 'QUARRY');
const housesCount = finalVillageState.objects.filter(o => o.type === 'HOUSE').length;
const producingCount = finalVillageState.objects.filter(o => PRODUCING_BUILDINGS.includes(o.type)).length;
const freeWorkers = housesCount - producingCount;
const tilesWithActions = finalVillageState.tiles.map(tile => {
const availableActions: any[] = [];