diff --git a/GEMINI.md b/GEMINI.md
index 39ebd3b..4b3a167 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -33,12 +33,14 @@ To run the development server with hot-reloading:
```bash
npm run dev
```
+Не запускай npm run dev сам. Скажи пользователю, что бы сделал это сам.
### Building for Production
To build the application for production:
```bash
npm run build
```
+Не запускай npm run build сам. Скажи пользователю, что бы сделал это сам.
## 3. Development Conventions
diff --git a/app/pages/village.vue b/app/pages/village.vue
index 7bfba09..7b28a42 100644
--- a/app/pages/village.vue
+++ b/app/pages/village.vue
@@ -35,7 +35,7 @@
{{ getActionLabel(action) }}
@@ -91,9 +91,37 @@ const getActionLabel = (action) => {
return action.type;
};
-const handleActionClick = (action) => {
- console.log('Action clicked:', action);
- // In a future task, this will dispatch the action to the backend.
+const isSubmitting = ref(false);
+
+const handleActionClick = async (action) => {
+ if (isSubmitting.value) return;
+
+ isSubmitting.value = true;
+ try {
+ const response = await useFetch('/api/village/action', {
+ method: 'POST',
+ body: {
+ tileId: selectedTile.value.id,
+ actionType: action.type,
+ payload: {
+ ...(action.type === 'BUILD' && { buildingType: action.buildingType }),
+ ...(action.type === 'MOVE' && { toTileId: action.toTileId }), // Assuming action.toTileId will be present for MOVE
+ },
+ },
+ });
+
+ if (response.error.value) {
+ alert(response.error.value.data?.statusMessage || 'An unknown error occurred.');
+ } else {
+ villageData.value = response.data.value;
+ selectedTile.value = null;
+ }
+ } catch (e) {
+ console.error('Failed to perform action:', e);
+ alert('An unexpected error occurred. Please check the console.');
+ } finally {
+ isSubmitting.value = false;
+ }
};
diff --git a/prisma/migrations/20260104131902_temporary_dummy_field/migration.sql b/prisma/migrations/20260104131902_temporary_dummy_field/migration.sql
new file mode 100644
index 0000000..d52726a
--- /dev/null
+++ b/prisma/migrations/20260104131902_temporary_dummy_field/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "dummy" TEXT;
diff --git a/prisma/migrations/20260104131928_remove_dummy_field/migration.sql b/prisma/migrations/20260104131928_remove_dummy_field/migration.sql
new file mode 100644
index 0000000..35b1d5d
--- /dev/null
+++ b/prisma/migrations/20260104131928_remove_dummy_field/migration.sql
@@ -0,0 +1,28 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `dummy` on the `User` table. All the data in the column will be lost.
+
+*/
+-- RedefineTables
+PRAGMA defer_foreign_keys=ON;
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "nickname" TEXT,
+ "avatar" TEXT DEFAULT '/avatars/default.png',
+ "coins" INTEGER NOT NULL DEFAULT 0,
+ "exp" INTEGER NOT NULL DEFAULT 0,
+ "soundOn" BOOLEAN NOT NULL DEFAULT true,
+ "confettiOn" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+INSERT INTO "new_User" ("avatar", "coins", "confettiOn", "createdAt", "email", "exp", "id", "nickname", "password", "soundOn", "updatedAt") SELECT "avatar", "coins", "confettiOn", "createdAt", "email", "exp", "id", "nickname", "password", "soundOn", "updatedAt" FROM "User";
+DROP TABLE "User";
+ALTER TABLE "new_User" RENAME TO "User";
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+PRAGMA foreign_keys=ON;
+PRAGMA defer_foreign_keys=OFF;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 47b063a..0b6e8e1 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -14,6 +14,9 @@ datasource db {
enum VillageObjectType {
HOUSE
FIELD
+ LUMBERJACK
+ QUARRY
+ WELL
}
enum TerrainType {
diff --git a/server/api/village/action.post.ts b/server/api/village/action.post.ts
new file mode 100644
index 0000000..17516ab
--- /dev/null
+++ b/server/api/village/action.post.ts
@@ -0,0 +1,45 @@
+// server/api/village/action.post.ts
+import { getUserIdFromSession } from '../../utils/auth';
+import { buildOnTile, clearTile, moveObject, removeObject } from '../../services/villageService';
+import { getVillageState } from '../../services/villageService';
+
+export default defineEventHandler(async (event) => {
+ const userId = await getUserIdFromSession(event);
+ const body = await readBody(event);
+
+ const { tileId, actionType, payload } = body;
+
+ if (!tileId || !actionType) {
+ throw createError({ statusCode: 400, statusMessage: 'Missing tileId or actionType' });
+ }
+
+ switch (actionType) {
+ case 'BUILD':
+ if (!payload?.buildingType) {
+ throw createError({ statusCode: 400, statusMessage: 'Missing buildingType for BUILD action' });
+ }
+ await buildOnTile(userId, tileId, payload.buildingType);
+ break;
+
+ case 'CLEAR':
+ await clearTile(userId, tileId);
+ break;
+
+ case 'MOVE':
+ if (!payload?.toTileId) {
+ throw createError({ statusCode: 400, statusMessage: 'Missing toTileId for MOVE action' });
+ }
+ await moveObject(userId, tileId, payload.toTileId);
+ break;
+
+ case 'REMOVE':
+ await removeObject(userId, tileId);
+ break;
+
+ default:
+ throw createError({ statusCode: 400, statusMessage: 'Invalid actionType' });
+ }
+
+ // Return the full updated village state
+ return getVillageState(userId);
+});
diff --git a/server/services/villageService.ts b/server/services/villageService.ts
index 84b4b01..d290632 100644
--- a/server/services/villageService.ts
+++ b/server/services/villageService.ts
@@ -295,5 +295,206 @@ export async function getVillageState(userId: number): Promise {
return { ...tile, availableActions };
});
- return { ...finalVillageState, tiles: tilesWithActions } as any;
-}
\ No newline at end of file
+ return { ...finalVillageState, tiles: tilesWithActions } as any;
+
+ }
+
+
+
+ // --- Action Service Functions ---
+
+
+
+ export async function buildOnTile(userId: number, tileId: number, buildingType: string) {
+
+ const { VillageObjectType } = await import('@prisma/client');
+
+ const validBuildingTypes = Object.keys(VillageObjectType);
+
+ if (!validBuildingTypes.includes(buildingType)) {
+
+ throw createError({ statusCode: 400, statusMessage: `Invalid building type: ${buildingType}` });
+
+ }
+
+
+
+ return prisma.$transaction(async (tx) => {
+
+ // 1. Fetch all necessary data
+
+ const user = await tx.user.findUniqueOrThrow({ where: { id: userId } });
+
+ const tile = await tx.villageTile.findUniqueOrThrow({ where: { id: tileId }, include: { village: true } });
+
+
+
+ // Ownership check
+
+ if (tile.village.userId !== userId) {
+
+ throw createError({ statusCode: 403, statusMessage: "You don't own this tile" });
+
+ }
+
+
+
+ // Business logic validation
+
+ if (tile.terrainType !== 'EMPTY' || tile.object) {
+
+ throw createError({ statusCode: 400, statusMessage: 'Tile is not empty' });
+
+ }
+
+
+
+ const cost = BUILDING_COSTS[buildingType];
+
+ if (user.coins < cost) {
+
+ throw createError({ statusCode: 400, statusMessage: 'Not enough coins' });
+
+ }
+
+
+
+ if (PRODUCING_BUILDINGS.includes(buildingType)) {
+
+ const villageObjects = await tx.villageObject.findMany({ where: { villageId: tile.villageId } });
+
+ const housesCount = villageObjects.filter(o => o.type === 'HOUSE').length;
+
+ const producingCount = villageObjects.filter(o => PRODUCING_BUILDINGS.includes(o.type)).length;
+
+ if (producingCount >= housesCount) {
+
+ throw createError({ statusCode: 400, statusMessage: 'Not enough workers (houses)' });
+
+ }
+
+ }
+
+
+
+ // 2. Perform mutations
+
+ await tx.user.update({
+
+ where: { id: userId },
+
+ data: { coins: { decrement: cost } },
+
+ });
+
+
+
+ await tx.villageObject.create({
+
+ data: {
+
+ type: buildingType as keyof typeof VillageObjectType,
+
+ villageId: tile.villageId,
+
+ tileId: tileId,
+
+ },
+
+ });
+
+ });
+
+ }
+
+
+
+ export async function clearTile(userId: number, tileId: number) {
+
+ return prisma.$transaction(async (tx) => {
+
+ const tile = await tx.villageTile.findUniqueOrThrow({
+
+ where: { id: tileId },
+
+ include: { village: { include: { objects: true } } },
+
+ });
+
+
+
+ if (tile.village.userId !== userId) {
+
+ throw createError({ statusCode: 403, statusMessage: "You don't own this tile" });
+
+ }
+
+
+
+ if (tile.terrainState !== 'IDLE') {
+
+ throw createError({ statusCode: 400, statusMessage: 'Tile is not idle' });
+
+ }
+
+
+
+ if (tile.terrainType === 'BLOCKED_TREE') {
+
+ const hasLumberjack = tile.village.objects.some(o => o.type === 'LUMBERJACK');
+
+ if (!hasLumberjack) throw createError({ statusCode: 400, statusMessage: 'Requires a Lumberjack to clear trees' });
+
+ } else if (tile.terrainType === 'BLOCKED_STONE') {
+
+ const hasQuarry = tile.village.objects.some(o => o.type === 'QUARRY');
+
+ if (!hasQuarry) throw createError({ statusCode: 400, statusMessage: 'Requires a Quarry to clear stones' });
+
+ } else {
+
+ throw createError({ statusCode: 400, statusMessage: 'Tile is not blocked by trees or stones' });
+
+ }
+
+
+
+ await tx.villageTile.update({
+
+ where: { id: tileId },
+
+ data: {
+
+ terrainState: 'CLEARING',
+
+ clearingStartedAt: new Date(),
+
+ },
+
+ });
+
+ });
+
+ }
+
+
+
+ export async function removeObject(userId: number, tileId: number) {
+
+ // As requested, this is a stub for now.
+
+ throw createError({ statusCode: 501, statusMessage: 'Remove action not implemented yet' });
+
+ }
+
+
+
+ export async function moveObject(userId: number, fromTileId: number, toTileId: number) {
+
+ // As requested, this is a stub for now.
+
+ throw createError({ statusCode: 501, statusMessage: 'Move action not implemented yet' });
+
+ }
+
+
\ No newline at end of file