diff --git a/backend/controllers/falukantController.js b/backend/controllers/falukantController.js index ca0bfc6..78bd545 100644 --- a/backend/controllers/falukantController.js +++ b/backend/controllers/falukantController.js @@ -181,6 +181,12 @@ class FalukantController { }); }); + this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId)); + this.buyVehicles = this._wrapWithUser( + (userId, req) => this.service.buyVehicles(userId, req.body), + { successStatus: 201 } + ); + } diff --git a/backend/models/associations.js b/backend/models/associations.js index 2c3692f..c1a2e0b 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -95,6 +95,9 @@ import PoliticalOfficeHistory from './falukant/log/political_office_history.js'; import ElectionHistory from './falukant/log/election_history.js'; import Underground from './falukant/data/underground.js'; import UndergroundType from './falukant/type/underground.js'; +import VehicleType from './falukant/type/vehicle.js'; +import Vehicle from './falukant/data/vehicle.js'; +import Transport from './falukant/data/transport.js'; import Blog from './community/blog.js'; import BlogPost from './community/blog_post.js'; import Campaign from './match3/campaign.js'; @@ -421,6 +424,71 @@ export default function setupAssociations() { PromotionalGiftLog.belongsTo(FalukantCharacter, { foreignKey: 'recipientCharacterId', as: 'recipient' }); FalukantCharacter.hasMany(PromotionalGiftLog, { foreignKey: 'recipientCharacterId', as: 'giftlogs' }); + // Vehicles & Transports + + VehicleType.hasMany(Vehicle, { + foreignKey: 'vehicleTypeId', + as: 'vehicles', + }); + Vehicle.belongsTo(VehicleType, { + foreignKey: 'vehicleTypeId', + as: 'type', + }); + + FalukantUser.hasMany(Vehicle, { + foreignKey: 'falukantUserId', + as: 'vehicles', + }); + Vehicle.belongsTo(FalukantUser, { + foreignKey: 'falukantUserId', + as: 'owner', + }); + + RegionData.hasMany(Vehicle, { + foreignKey: 'regionId', + as: 'vehicles', + }); + Vehicle.belongsTo(RegionData, { + foreignKey: 'regionId', + as: 'region', + }); + + Transport.belongsTo(RegionData, { + foreignKey: 'sourceRegionId', + as: 'sourceRegion', + }); + Transport.belongsTo(RegionData, { + foreignKey: 'targetRegionId', + as: 'targetRegion', + }); + + RegionData.hasMany(Transport, { + foreignKey: 'sourceRegionId', + as: 'outgoingTransports', + }); + RegionData.hasMany(Transport, { + foreignKey: 'targetRegionId', + as: 'incomingTransports', + }); + + Transport.belongsTo(ProductType, { + foreignKey: 'productId', + as: 'productType', + }); + ProductType.hasMany(Transport, { + foreignKey: 'productId', + as: 'transports', + }); + + Transport.belongsTo(Vehicle, { + foreignKey: 'vehicleId', + as: 'vehicle', + }); + Vehicle.hasMany(Transport, { + foreignKey: 'vehicleId', + as: 'transports', + }); + PromotionalGift.hasMany(PromotionalGiftCharacterTrait, { foreignKey: 'gift_id', as: 'characterTraits' }); PromotionalGift.hasMany(PromotionalGiftMood, { foreignKey: 'gift_id', as: 'promotionalgiftmoods' }); diff --git a/backend/models/falukant/data/transport.js b/backend/models/falukant/data/transport.js new file mode 100644 index 0000000..82b1dcc --- /dev/null +++ b/backend/models/falukant/data/transport.js @@ -0,0 +1,41 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +class Transport extends Model {} + +Transport.init( + { + sourceRegionId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + targetRegionId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + productId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + size: { + type: DataTypes.INTEGER, + allowNull: false, + }, + vehicleId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'Transport', + tableName: 'transport', + schema: 'falukant_data', + timestamps: true, + underscored: true, + } +); + +export default Transport; + + diff --git a/backend/models/falukant/data/vehicle.js b/backend/models/falukant/data/vehicle.js new file mode 100644 index 0000000..e5d6a19 --- /dev/null +++ b/backend/models/falukant/data/vehicle.js @@ -0,0 +1,33 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +class Vehicle extends Model {} + +Vehicle.init( + { + vehicleTypeId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + falukantUserId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + regionId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'Vehicle', + tableName: 'vehicle', + schema: 'falukant_data', + timestamps: true, + underscored: true, + } +); + +export default Vehicle; + + diff --git a/backend/models/falukant/type/vehicle.js b/backend/models/falukant/type/vehicle.js new file mode 100644 index 0000000..85e7c6c --- /dev/null +++ b/backend/models/falukant/type/vehicle.js @@ -0,0 +1,46 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +class VehicleType extends Model {} + +VehicleType.init( + { + tr: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + cost: { + // base purchase cost of the vehicle + type: DataTypes.INTEGER, + allowNull: false, + }, + capacity: { + // transport capacity (e.g. in units of goods) + type: DataTypes.INTEGER, + allowNull: false, + }, + transportMode: { + // e.g. 'land', 'water', 'air' + type: DataTypes.STRING, + allowNull: false, + }, + speed: { + // abstract speed value, higher = faster + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'VehicleType', + tableName: 'vehicle', + schema: 'falukant_type', + timestamps: false, + underscored: true, + } +); + +export default VehicleType; + + diff --git a/backend/models/index.js b/backend/models/index.js index 7616c56..183a000 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -113,6 +113,9 @@ import PoliticalOfficeHistory from './falukant/log/political_office_history.js'; import ElectionHistory from './falukant/log/election_history.js'; import UndergroundType from './falukant/type/underground.js'; import Underground from './falukant/data/underground.js'; +import VehicleType from './falukant/type/vehicle.js'; +import Vehicle from './falukant/data/vehicle.js'; +import Transport from './falukant/data/transport.js'; import Room from './chat/room.js'; import ChatUser from './chat/user.js'; @@ -207,6 +210,9 @@ const models = { Credit, DebtorsPrism, HealthActivity, + VehicleType, + Vehicle, + Transport, PoliticalOfficeType, PoliticalOfficeRequirement, PoliticalOfficeBenefitType, diff --git a/backend/routers/falukantRouter.js b/backend/routers/falukantRouter.js index 6a0e045..32ede58 100644 --- a/backend/routers/falukantRouter.js +++ b/backend/routers/falukantRouter.js @@ -69,6 +69,8 @@ router.post('/politics/elections', falukantController.vote); router.get('/politics/open', falukantController.getOpenPolitics); router.post('/politics/open', falukantController.applyForElections); router.get('/cities', falukantController.getRegions); +router.get('/vehicles/types', falukantController.getVehicleTypes); +router.post('/vehicles', falukantController.buyVehicles); router.get('/underground/types', falukantController.getUndergroundTypes); router.get('/notifications', falukantController.getNotifications); router.get('/notifications/all', falukantController.getAllNotifications); diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 3bad0a5..ecc68f5 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -57,6 +57,8 @@ import UndergroundType from '../models/falukant/type/underground.js'; import Notification from '../models/falukant/log/notification.js'; import PoliticalOffice from '../models/falukant/data/political_office.js'; import Underground from '../models/falukant/data/underground.js'; +import VehicleType from '../models/falukant/type/vehicle.js'; +import Vehicle from '../models/falukant/data/vehicle.js'; function calcAge(birthdate) { const b = new Date(birthdate); b.setHours(0, 0); @@ -448,6 +450,55 @@ class FalukantService extends BaseService { return FalukantStock.findAll({ where: { regionId: b.regionId, userId: u.id } }); } + async getVehicleTypes(hashedUserId) { + // Validate user existence, but we don't filter by user here + await getFalukantUserOrFail(hashedUserId); + return VehicleType.findAll(); + } + + async buyVehicles(hashedUserId, { vehicleTypeId, quantity, regionId }) { + const user = await getFalukantUserOrFail(hashedUserId); + const qty = Math.max(1, parseInt(quantity, 10) || 0); + + const type = await VehicleType.findByPk(vehicleTypeId); + if (!type) { + throw new Error('Vehicle type not found'); + } + + const totalCost = type.cost * qty; + if (user.money < totalCost) { + throw new PreconditionError('insufficientFunds'); + } + + // Ensure the region exists (and is part of Falukant map) + const region = await RegionData.findByPk(regionId); + if (!region) { + throw new Error('Region not found'); + } + + // Update money and create vehicles in a transaction + await sequelize.transaction(async (tx) => { + await updateFalukantUserMoney( + user.id, + -totalCost, + 'buy_vehicles', + user.id + ); + + const records = []; + for (let i = 0; i < qty; i++) { + records.push({ + vehicleTypeId: type.id, + falukantUserId: user.id, + regionId: region.id, + }); + } + await Vehicle.bulkCreate(records, { transaction: tx }); + }); + + return { success: true }; + } + async createStock(hashedUserId, branchId, stockData) { const u = await getFalukantUserOrFail(hashedUserId); const b = await getBranchOrFail(u.id, branchId); diff --git a/backend/utils/falukant/initializeFalukantTypes.js b/backend/utils/falukant/initializeFalukantTypes.js index 9c2bc67..5eda435 100644 --- a/backend/utils/falukant/initializeFalukantTypes.js +++ b/backend/utils/falukant/initializeFalukantTypes.js @@ -12,6 +12,7 @@ import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js"; import PartyType from "../../models/falukant/type/party.js"; import MusicType from "../../models/falukant/type/music.js"; import BanquetteType from "../../models/falukant/type/banquette.js"; +import VehicleType from "../../models/falukant/type/vehicle.js"; import LearnRecipient from "../../models/falukant/type/learn_recipient.js"; import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js"; import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js"; @@ -41,6 +42,7 @@ export const initializeFalukantTypes = async () => { await initializePoliticalOfficeTypes(); await initializePoliticalOfficePrerequisites(); await initializeUndergroundTypes(); + await initializeVehicleTypes(); }; const regionTypes = []; @@ -273,6 +275,16 @@ const learnerTypes = [ { tr: 'director', }, ]; +const vehicleTypes = [ + { tr: 'cargo_cart', name: 'Lastkarren', cost: 100, capacity: 20, transportMode: 'land', speed: 1 }, + { tr: 'ox_cart', name: 'Ochsenkarren', cost: 200, capacity: 50, transportMode: 'land', speed: 2 }, + { tr: 'small_carriage', name: 'kleine Pferdekutsche', cost: 300, capacity: 35, transportMode: 'land', speed: 3 }, + { tr: 'large_carriage', name: 'große Pferdekutsche', cost: 1000, capacity: 100, transportMode: 'land', speed: 3 }, + { tr: 'four_horse_carriage', name: 'Vierspänner', cost: 5000, capacity: 200, transportMode: 'land', speed: 4 }, + { tr: 'raft', name: 'Floß', cost: 100, capacity: 25, transportMode: 'water', speed: 1 }, + { tr: 'sailing_ship', name: 'Segelschiff', cost: 500, capacity: 200, transportMode: 'water', speed: 3 }, +]; + const politicalOfficeBenefitTypes = [ { tr: 'salary' }, { tr: 'reputation' }, @@ -883,6 +895,21 @@ export const initializeLearnerTypes = async () => { } }; +export const initializeVehicleTypes = async () => { + for (const v of vehicleTypes) { + await VehicleType.findOrCreate({ + where: { tr: v.tr }, + defaults: { + tr: v.tr, + cost: v.cost, + capacity: v.capacity, + transportMode: v.transportMode, + speed: v.speed, + }, + }); + } +}; + export const initializePoliticalOfficeBenefitTypes = async () => { for (const benefitType of politicalOfficeBenefitTypes) { await PoliticalOfficeBenefitType.findOrCreate({ diff --git a/frontend/src/dialogues/falukant/BuyVehicleDialog.vue b/frontend/src/dialogues/falukant/BuyVehicleDialog.vue new file mode 100644 index 0000000..5568016 --- /dev/null +++ b/frontend/src/dialogues/falukant/BuyVehicleDialog.vue @@ -0,0 +1,191 @@ + + + + + + + diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index f7e19a9..3b3f96c 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -106,7 +106,8 @@ "director": "Direktor", "inventory": "Inventar", "production": "Produktion", - "storage": "Lager" + "storage": "Lager", + "transport": "Transportmittel" }, "selection": { "title": "Niederlassungsauswahl", @@ -203,6 +204,25 @@ "buycost": "Kosten", "sellincome": "Einnahmen" }, + "vehicles": { + "cargo_cart": "Lastkarren", + "ox_cart": "Ochsenkarren", + "small_carriage": "Kleine Pferdekutsche", + "large_carriage": "Große Pferdekutsche", + "four_horse_carriage": "Vierspänner", + "raft": "Floß", + "sailing_ship": "Segelschiff" + }, + "transport": { + "title": "Transportmittel", + "placeholder": "Hier kannst du Transportmittel für deine Region kaufen.", + "vehicleType": "Transportmittel", + "quantity": "Anzahl", + "totalCost": "Gesamtkosten", + "notEnoughMoney": "Du hast nicht genug Geld für diesen Kauf.", + "buy": "Transportmittel kaufen", + "balance": "Kontostand" + }, "stocktype": { "wood": "Holzlager", "stone": "Steinlager", diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index 3264206..b4f1f22 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -30,6 +30,17 @@ "knowledge": "Knowledge", "hire": "Hire", "noProposals": "No director candidates available." + }, + "branch": { + "vehicles": { + "cargo_cart": "Cargo cart", + "ox_cart": "Ox cart", + "small_carriage": "Small horse carriage", + "large_carriage": "Large horse carriage", + "four_horse_carriage": "Four-horse carriage", + "raft": "Raft", + "sailing_ship": "Sailing ship" + } } } } \ No newline at end of file diff --git a/frontend/src/views/falukant/BranchView.vue b/frontend/src/views/falukant/BranchView.vue index b331d05..798c6fe 100644 --- a/frontend/src/views/falukant/BranchView.vue +++ b/frontend/src/views/falukant/BranchView.vue @@ -51,7 +51,21 @@
+ + +
+

{{ $t('falukant.branch.transport.placeholder') }}

+ +
+ @@ -65,6 +79,7 @@ import SaleSection from '@/components/falukant/SaleSection.vue'; import ProductionSection from '@/components/falukant/ProductionSection.vue'; import StorageSection from '@/components/falukant/StorageSection.vue'; import RevenueSection from '@/components/falukant/RevenueSection.vue'; +import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue'; import apiClient from '@/utils/axios.js'; import { mapState } from 'vuex'; @@ -79,8 +94,9 @@ export default { ProductionSection, StorageSection, RevenueSection, + BuyVehicleDialog, }, - + data() { return { branches: [], @@ -92,6 +108,7 @@ export default { { value: 'inventory', label: 'falukant.branch.tabs.inventory' }, { value: 'director', label: 'falukant.branch.tabs.director' }, { value: 'storage', label: 'falukant.branch.tabs.storage' }, + { value: 'transport', label: 'falukant.branch.tabs.transport' }, ], }; }, @@ -155,6 +172,7 @@ export default { const result = await apiClient.get('/api/falukant/branches'); this.branches = result.data.map(branch => ({ id: branch.id, + regionId: branch.regionId, cityName: branch.region.name, type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`), isMainBranch: branch.isMainBranch, @@ -325,6 +343,16 @@ export default { console.error('Error processing daemon message:', error); } }, + + openBuyVehicleDialog() { + if (!this.selectedBranch) return; + this.$refs.buyVehicleDialog?.open(); + }, + + handleVehiclesBought() { + // Refresh status bar (for updated money) and potentially other data later + this.$refs.statusBar?.fetchStatus(); + }, }, }; @@ -333,4 +361,4 @@ export default { h2 { padding-top: 20px; } - \ No newline at end of file + diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 04615f2..37c59c6 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,6 +7,8 @@ import path from 'path'; export default defineConfig(({ mode }) => { // Lade Umgebungsvariablen explizit const env = loadEnv(mode, process.cwd(), ''); + // Kombiniere mit process.env, um auch Shell-Umgebungsvariablen zu berücksichtigen + const combinedEnv = { ...env, ...process.env }; return { plugins: [vue()], @@ -16,18 +18,18 @@ export default defineConfig(({ mode }) => { 'import.meta.env.DEV': false, 'import.meta.env.PROD': true, 'import.meta.env.MODE': '"production"', - // Stelle sicher, dass Umgebungsvariablen aus .env.production geladen werden - ...(env.VITE_DAEMON_SOCKET && { - 'import.meta.env.VITE_DAEMON_SOCKET': JSON.stringify(env.VITE_DAEMON_SOCKET) + // Stelle sicher, dass Umgebungsvariablen aus .env.production oder Shell-Umgebung geladen werden + ...(combinedEnv.VITE_DAEMON_SOCKET && { + 'import.meta.env.VITE_DAEMON_SOCKET': JSON.stringify(combinedEnv.VITE_DAEMON_SOCKET) }), - ...(env.VITE_API_BASE_URL && { - 'import.meta.env.VITE_API_BASE_URL': JSON.stringify(env.VITE_API_BASE_URL) + ...(combinedEnv.VITE_API_BASE_URL && { + 'import.meta.env.VITE_API_BASE_URL': JSON.stringify(combinedEnv.VITE_API_BASE_URL) }), - ...(env.VITE_CHAT_WS_URL && { - 'import.meta.env.VITE_CHAT_WS_URL': JSON.stringify(env.VITE_CHAT_WS_URL) + ...(combinedEnv.VITE_CHAT_WS_URL && { + 'import.meta.env.VITE_CHAT_WS_URL': JSON.stringify(combinedEnv.VITE_CHAT_WS_URL) }), - ...(env.VITE_SOCKET_IO_URL && { - 'import.meta.env.VITE_SOCKET_IO_URL': JSON.stringify(env.VITE_SOCKET_IO_URL) + ...(combinedEnv.VITE_SOCKET_IO_URL && { + 'import.meta.env.VITE_SOCKET_IO_URL': JSON.stringify(combinedEnv.VITE_SOCKET_IO_URL) }) }, optimizeDeps: {