правки по админ тулу

This commit is contained in:
Alexander Andreev 2026-01-04 18:26:52 +03:00
parent 365d04b678
commit a8241d93c6
6 changed files with 260 additions and 40 deletions

View File

@ -54,6 +54,29 @@
<button @click="handleResetVillage" :disabled="isSubmittingAdminAction">Reset Village</button>
<button @click="handleCompleteClearing" :disabled="isSubmittingAdminAction">Complete All Clearing</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>
@ -65,6 +88,11 @@ const { data: villageData, pending, error, refresh: refreshVillageData } = await
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) => {
@ -122,6 +150,7 @@ const handleActionClick = async (action) => {
} else {
villageData.value = response.data.value;
selectedTile.value = null;
await refreshEvents(); // Refresh the event log
}
} catch (e) {
console.error('Failed to perform action:', e);
@ -133,7 +162,7 @@ const handleActionClick = async (action) => {
const isSubmittingAdminAction = ref(false);
async function handleAdminAction(url: string) {
async function handleAdminAction(url) {
if (isSubmittingAdminAction.value) return;
isSubmittingAdminAction.value = true;
@ -142,7 +171,8 @@ async function handleAdminAction(url: string) {
if (error.value) {
alert(error.value.data?.statusMessage || 'An admin action failed.');
} else {
await refreshVillageData();
// Refresh both data sources in parallel
await Promise.all([refreshVillageData(), refreshEvents()]);
}
} catch (e) {
console.error('Failed to perform admin action:', e);
@ -355,4 +385,31 @@ const handleCompleteClearing = () => handleAdminAction('/api/admin/village/compl
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;
}
</style>

View File

@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "VillageEvent" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"villageId" INTEGER NOT NULL,
"type" TEXT NOT NULL,
"message" TEXT NOT NULL,
"tileX" INTEGER,
"tileY" INTEGER,
"coins" INTEGER NOT NULL,
"exp" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "VillageEvent_villageId_fkey" FOREIGN KEY ("villageId") REFERENCES "Village" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

View File

@ -113,6 +113,7 @@ model Village {
userId Int @unique // Each user has only one village
objects VillageObject[]
tiles VillageTile[]
events VillageEvent[]
}
// VillageObject: An object (e.g., house, field) placed on a village tile.
@ -148,3 +149,21 @@ model VillageTile {
@@unique([villageId, x, y])
}
model VillageEvent {
id Int @id @default(autoincrement())
villageId Int
type String
message String
tileX Int?
tileY Int?
coins Int
exp Int
createdAt DateTime @default(now())
village Village @relation(fields: [villageId], references: [id], onDelete: Cascade)
}

View File

@ -1,6 +1,7 @@
// server/api/admin/village/complete-clearing.post.ts
import { getUserIdFromSession } from '../../../utils/auth';
import { PrismaClient } from '@prisma/client';
import { REWARDS } from '../../../services/villageService';
const prisma = new PrismaClient();
@ -25,27 +26,40 @@ export default defineEventHandler(async (event) => {
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({
const totalCoins = tilesToComplete.length * REWARDS.CLEARING.coins;
const totalExp = tilesToComplete.length * REWARDS.CLEARING.exp;
await prisma.$transaction(async (tx) => {
// 1. Update user totals
await tx.user.update({
where: { id: userId },
data: {
coins: { increment: tilesToComplete.length },
exp: { increment: tilesToComplete.length },
coins: { increment: totalCoins },
exp: { increment: totalExp },
},
}),
]);
});
// 2. Update all the tiles
await tx.villageTile.updateMany({
where: { id: { in: tilesToComplete.map(t => t.id) } },
data: { terrainState: 'IDLE', terrainType: 'EMPTY', clearingStartedAt: null },
});
// 3. Create an event for each completed tile
for (const tile of tilesToComplete) {
await tx.villageEvent.create({
data: {
villageId: village.id,
type: tile.terrainType === 'BLOCKED_TREE' ? 'CLEAR_TREE' : 'CLEAR_STONE',
message: `Finished clearing ${tile.terrainType === 'BLOCKED_TREE' ? 'a tree' : 'a stone'} at (${tile.x}, ${tile.y})`,
tileX: tile.x,
tileY: tile.y,
coins: REWARDS.CLEARING.coins,
exp: REWARDS.CLEARING.exp,
}
});
}
});
return { success: true, message: `Completed ${tilesToComplete.length} clearing tasks.` };
});

View File

@ -0,0 +1,30 @@
// server/api/village/events.get.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);
const village = await prisma.village.findUnique({
where: { userId },
});
if (!village) {
// Or return empty array, depending on desired behavior for users without villages yet
return [];
}
const events = await prisma.villageEvent.findMany({
where: {
villageId: village.id,
},
orderBy: {
createdAt: 'desc',
},
take: 50, // Limit to the last 50 events to avoid overloading the client
});
return events;
});

View File

@ -21,6 +21,10 @@ export const PRODUCING_BUILDINGS: string[] = [
'QUARRY',
];
export const REWARDS = {
CLEARING: { coins: 1, exp: 1 },
};
// Helper to get the start of a given date for daily EXP checks
const getStartOfDay = (date: Date) => {
const d = new Date(date);
@ -118,21 +122,40 @@ export async function getVillageState(userId: number): Promise<FullVillage> {
);
if (finishedClearingTiles.length > 0) {
await prisma.$transaction([
prisma.user.update({
const totalCoins = finishedClearingTiles.length * REWARDS.CLEARING.coins;
const totalExp = finishedClearingTiles.length * REWARDS.CLEARING.exp;
await prisma.$transaction(async (tx) => {
// 1. Update user totals
await tx.user.update({
where: { id: userId },
data: {
coins: { increment: finishedClearingTiles.length },
exp: { increment: finishedClearingTiles.length },
coins: { increment: totalCoins },
exp: { increment: totalExp },
},
}),
...finishedClearingTiles.map(t =>
prisma.villageTile.update({
where: { id: t.id },
});
// 2. Update all the tiles
await tx.villageTile.updateMany({
where: { id: { in: finishedClearingTiles.map(t => t.id) } },
data: { terrainType: 'EMPTY', terrainState: 'IDLE', clearingStartedAt: null },
})
),
]);
});
// 3. Create an event for each completed tile
for (const tile of finishedClearingTiles) {
await tx.villageEvent.create({
data: {
villageId: villageSnapshot.id,
type: tile.terrainType === 'BLOCKED_TREE' ? 'CLEAR_TREE' : 'CLEAR_STONE',
message: `Finished clearing ${tile.terrainType === 'BLOCKED_TREE' ? 'a tree' : 'a stone'} at (${tile.x}, ${tile.y})`,
tileX: tile.x,
tileY: tile.y,
coins: REWARDS.CLEARING.coins,
exp: REWARDS.CLEARING.exp,
}
});
}
});
}
// --- Step 3: Refetch for next logic step ---
@ -380,20 +403,84 @@ export async function getVillageState(userId: number): Promise<FullVillage> {
await tx.villageObject.create({
data: {
type: buildingType as keyof typeof VillageObjectType,
villageId: tile.villageId,
tileId: tileId,
},
});
});
await tx.villageEvent.create({
data: {
villageId: tile.villageId,
type: `BUILD_${buildingType}`,
message: `Built a ${buildingType} at (${tile.x}, ${tile.y})`,
tileX: tile.x,
tileY: tile.y,
coins: -cost,
exp: 0,
},
});
});
}