From a8241d93c66237e22f220b3bc5f155193aaa4843 Mon Sep 17 00:00:00 2001 From: Alexander Andreev Date: Sun, 4 Jan 2026 18:26:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=20=D1=82=D1=83=D0=BB?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pages/village.vue | 61 ++++++++- .../migration.sql | 13 ++ prisma/schema.prisma | 19 +++ .../admin/village/complete-clearing.post.ts | 50 ++++--- server/api/village/events.get.ts | 30 +++++ server/services/villageService.ts | 127 +++++++++++++++--- 6 files changed, 260 insertions(+), 40 deletions(-) create mode 100644 prisma/migrations/20260104143245_add_village_events/migration.sql create mode 100644 server/api/village/events.get.ts diff --git a/app/pages/village.vue b/app/pages/village.vue index ec0787c..747b20e 100644 --- a/app/pages/village.vue +++ b/app/pages/village.vue @@ -54,6 +54,29 @@ + + +
+

Activity Log

+ + + + + + + + + + + + + + + + + +
DateEventCoinsEXP
{{ new Date(event.createdAt).toLocaleString() }}{{ event.message }}{{ event.coins }}{{ event.exp }}
+
@@ -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; +} \ No newline at end of file diff --git a/prisma/migrations/20260104143245_add_village_events/migration.sql b/prisma/migrations/20260104143245_add_village_events/migration.sql new file mode 100644 index 0000000..7f8911d --- /dev/null +++ b/prisma/migrations/20260104143245_add_village_events/migration.sql @@ -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 +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0b6e8e1..d5d16a5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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) +} diff --git a/server/api/admin/village/complete-clearing.post.ts b/server/api/admin/village/complete-clearing.post.ts index a8e7f1a..09bc3fe 100644 --- a/server/api/admin/village/complete-clearing.post.ts +++ b/server/api/admin/village/complete-clearing.post.ts @@ -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.` }; }); diff --git a/server/api/village/events.get.ts b/server/api/village/events.get.ts new file mode 100644 index 0000000..4682601 --- /dev/null +++ b/server/api/village/events.get.ts @@ -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; +}); diff --git a/server/services/villageService.ts b/server/services/villageService.ts index 8e27781..8397bbe 100644 --- a/server/services/villageService.ts +++ b/server/services/villageService.ts @@ -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 { ); 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 --- @@ -378,23 +401,87 @@ export async function getVillageState(userId: number): Promise { - await tx.villageObject.create({ + await tx.villageObject.create({ - data: { + - type: buildingType as keyof typeof VillageObjectType, + data: { - villageId: tile.villageId, + - tileId: tileId, + 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, + + + + }, + + + + }); + + + + }); + + + + }