добавлен админ тул
This commit is contained in:
parent
4b83e8339f
commit
365d04b678
|
|
@ -33,7 +33,7 @@ To run the development server with hot-reloading:
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
Не запускай npm run dev сам. Скажи пользователю, что бы сделал это сам.
|
Никогда НЕ запускай npm run dev сам. Скажи пользователю, что бы сделал это сам.
|
||||||
|
|
||||||
### Building for Production
|
### Building for Production
|
||||||
To build the application for production:
|
To build the application for production:
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
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,
|
lazy: true,
|
||||||
server: false, // Ensure this runs on the client-side
|
server: false, // Ensure this runs on the client-side
|
||||||
});
|
});
|
||||||
|
|
@ -123,6 +130,30 @@ const handleActionClick = async (action) => {
|
||||||
isSubmitting.value = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -291,4 +322,37 @@ const handleActionClick = async (action) => {
|
||||||
.close-overlay-button:hover {
|
.close-overlay-button:hover {
|
||||||
background-color: #5a6268;
|
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>
|
</style>
|
||||||
51
server/api/admin/village/complete-clearing.post.ts
Normal file
51
server/api/admin/village/complete-clearing.post.ts
Normal 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.` };
|
||||||
|
});
|
||||||
33
server/api/admin/village/reset.post.ts
Normal file
33
server/api/admin/village/reset.post.ts
Normal 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.' };
|
||||||
|
});
|
||||||
|
|
@ -166,56 +166,41 @@ 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 } } } })!;
|
villageSnapshot = await prisma.village.findUnique({ where: { userId }, include: { user: true, tiles: { include: { object: true } }, objects: { include: { tile: true } } } })!;
|
||||||
|
|
||||||
// --- Step 6: Auto-start Terrain Cleaning ---
|
// --- Step 6: Auto-start Terrain Cleaning ---
|
||||||
const housesCount = villageSnapshot.objects.filter(o => o.type === 'HOUSE').length;
|
const lumberjackCount = villageSnapshot.objects.filter(o => o.type === 'LUMBERJACK').length;
|
||||||
const producingCount = villageSnapshot.objects.filter(o => PRODUCING_BUILDINGS.includes(o.type)).length;
|
const quarryCount = villageSnapshot.objects.filter(o => o.type === 'QUARRY').length;
|
||||||
const freeWorkers = housesCount - producingCount;
|
|
||||||
|
|
||||||
if (producingCount <= housesCount) {
|
const clearingTreesCount = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_TREE' && t.terrainState === 'CLEARING').length;
|
||||||
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 clearingStonesCount = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_STONE' && t.terrainState === 'CLEARING').length;
|
||||||
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 assignTasks = (workers: (VillageObject & { tile: VillageTile })[], targets: VillageTile[], newlyTargeted: Set<number>) => {
|
const freeLumberjacks = lumberjackCount - clearingTreesCount;
|
||||||
workers.forEach(worker => {
|
const freeQuarries = quarryCount - clearingStonesCount;
|
||||||
const potentialTargets = targets
|
|
||||||
.filter(t => !newlyTargeted.has(t.id))
|
|
||||||
.map(target => ({ target, distance: manhattanDistance(worker.tile, target) }))
|
|
||||||
.sort((a, b) => a.distance - b.distance);
|
|
||||||
|
|
||||||
if (!potentialTargets.length) return;
|
|
||||||
|
|
||||||
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 })[];
|
|
||||||
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>();
|
const tileIdsToClear = new Set<number>();
|
||||||
|
|
||||||
assignTasks(lumberjacks, idleTrees, tileIdsToClear);
|
if (freeLumberjacks > 0) {
|
||||||
assignTasks(quarries, idleStones, tileIdsToClear);
|
const idleTrees = villageSnapshot.tiles.filter(t => t.terrainType === 'BLOCKED_TREE' && t.terrainState === 'IDLE');
|
||||||
|
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) {
|
if (tileIdsToClear.size > 0) {
|
||||||
await prisma.villageTile.updateMany({
|
await prisma.villageTile.updateMany({
|
||||||
where: { id: { in: Array.from(tileIdsToClear) } },
|
where: { id: { in: Array.from(tileIdsToClear) } },
|
||||||
data: { terrainState: 'CLEARING', clearingStartedAt: now },
|
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 ---
|
// --- Step 7: Final Fetch & Action Enrichment ---
|
||||||
|
|
@ -237,6 +222,10 @@ export async function getVillageState(userId: number): Promise<FullVillage> {
|
||||||
const hasLumberjack = finalVillageState.objects.some(o => o.type === 'LUMBERJACK');
|
const hasLumberjack = finalVillageState.objects.some(o => o.type === 'LUMBERJACK');
|
||||||
const hasQuarry = finalVillageState.objects.some(o => o.type === 'QUARRY');
|
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 tilesWithActions = finalVillageState.tiles.map(tile => {
|
||||||
const availableActions: any[] = [];
|
const availableActions: any[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user