Add raid transport feature and related updates: Introduce new raid transport functionality in FalukantService and FalukantController, including methods for retrieving raid transport regions and handling guard counts. Update frontend components to support guard count input and display related costs. Enhance localization files to include new terms for raid transport and associated metrics in English, German, and Spanish.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 18:47:01 +01:00
parent 43dd1a3b7f
commit de52b6f26d
18 changed files with 886 additions and 82 deletions

View File

@@ -252,6 +252,7 @@ class FalukantController {
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(userId));
this.getRaidTransportRegions = this._wrapWithUser((userId) => this.service.getRaidTransportRegions(userId));
this.getUndergroundActivities = this._wrapWithUser((userId) => this.service.getUndergroundActivities(userId));
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));

View File

@@ -0,0 +1,22 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.transport
ADD COLUMN IF NOT EXISTS guard_count integer NOT NULL DEFAULT 0;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.underground
ALTER COLUMN victim_id DROP NOT NULL;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.transport
DROP COLUMN IF EXISTS guard_count;
`);
}
};

View File

@@ -25,6 +25,11 @@ Transport.init(
type: DataTypes.INTEGER,
allowNull: false,
},
guardCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
},
{
sequelize,
@@ -38,4 +43,3 @@ Transport.init(
export default Transport;

View File

@@ -12,7 +12,7 @@ Underground.init({
allowNull: false},
victimId: {
type: DataTypes.INTEGER,
allowNull: false},
allowNull: true},
parameters: {
type: DataTypes.JSON,
allowNull: true},

View File

@@ -112,6 +112,7 @@ router.post('/transports', falukantController.createTransport);
router.get('/transports/route', falukantController.getTransportRoute);
router.get('/transports/branch/:branchId', falukantController.getBranchTransports);
router.get('/underground/types', falukantController.getUndergroundTypes);
router.get('/underground/raid-regions', falukantController.getRaidTransportRegions);
router.get('/underground/activities', falukantController.getUndergroundActivities);
router.get('/notifications', falukantController.getNotifications);
router.get('/notifications/all', falukantController.getAllNotifications);

View File

@@ -1330,8 +1330,9 @@ class FalukantService extends BaseService {
};
}
async createTransport(hashedUserId, { branchId, vehicleTypeId, vehicleIds, productId, quantity, targetBranchId }) {
async createTransport(hashedUserId, { branchId, vehicleTypeId, vehicleIds, productId, quantity, targetBranchId, guardCount: rawGuardCount }) {
const user = await getFalukantUserOrFail(hashedUserId);
const guardCount = Math.max(0, Number.parseInt(rawGuardCount ?? 0, 10) || 0);
const sourceBranch = await Branch.findOne({
where: { id: branchId, falukantUserId: user.id },
@@ -1509,6 +1510,9 @@ class FalukantService extends BaseService {
hardMax = 1;
}
transportCost += guardCount * 4;
transportCost = Math.round(transportCost * 100) / 100;
if (user.money < transportCost) {
throw new PreconditionError('insufficientFunds');
}
@@ -1538,6 +1542,7 @@ class FalukantService extends BaseService {
productId: isEmptyTransport ? null : productId,
size: isEmptyTransport ? 0 : size,
vehicleId: v.id,
guardCount,
},
{ transaction: tx }
);
@@ -1601,6 +1606,7 @@ class FalukantService extends BaseService {
id: t.id,
size: t.size,
vehicleId: t.vehicleId,
guardCount: t.guardCount,
})),
};
});
@@ -1669,6 +1675,7 @@ class FalukantService extends BaseService {
targetRegion: t.targetRegion,
product: t.productType,
size: t.size,
guardCount: Number(t.guardCount || 0),
vehicleId: t.vehicleId,
vehicleType: t.vehicle?.type || null,
createdAt: t.createdAt,
@@ -6529,6 +6536,34 @@ ORDER BY r.id`,
return undergroundTypes;
}
async getRaidTransportRegions(hashedUserId) {
await getFalukantUserOrFail(hashedUserId);
const regions = await RegionData.findAll({
where: {
regionTypeId: { [Op.in]: [4, 5] }
},
include: [
{
model: RegionType,
as: 'regionType',
attributes: ['id', 'labelTr']
}
],
attributes: ['id', 'name', 'regionTypeId'],
order: [['name', 'ASC']]
});
return regions
.filter((region) => region.regionType?.labelTr !== 'town')
.map((region) => ({
id: region.id,
name: region.name,
regionTypeId: region.regionTypeId,
regionTypeLabel: region.regionType?.labelTr || null
}));
}
async getNotifications(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
@@ -6768,7 +6803,7 @@ ORDER BY r.id`,
}
async createUndergroundActivity(hashedUserId, payload) {
const { typeId, victimUsername, target, goal, politicalTargets } = payload;
const { typeId, victimUsername, target, goal, politicalTargets, regionId, bandSize } = payload;
// 1) Performer auflösen
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
@@ -6777,42 +6812,43 @@ ORDER BY r.id`,
}
const performerChar = falukantUser.character;
// 2) Victim auflösen über Username (inner join)
const victimChar = await FalukantCharacter.findOne({
include: [
{
model: FalukantUser,
as: 'user',
required: true, // inner join
attributes: [],
include: [
{
model: User,
as: 'user',
required: true, // inner join
where: { username: victimUsername },
attributes: []
}
]
}
]
});
if (!victimChar) {
throw new PreconditionError('Victim character not found');
}
// 3) Selbstangriff verhindern
if (victimChar.id === performerChar.id) {
throw new PreconditionError('Cannot target yourself');
}
// 4) Typ-spezifische Validierung
// 2) Typ-spezifische Validierung
const undergroundType = await UndergroundType.findByPk(typeId);
if (!undergroundType) {
throw new Error('Invalid underground type');
}
let victimChar = null;
if (undergroundType.tr !== 'raid_transport') {
victimChar = await FalukantCharacter.findOne({
include: [
{
model: FalukantUser,
as: 'user',
required: true,
attributes: [],
include: [
{
model: User,
as: 'user',
required: true,
where: { username: victimUsername },
attributes: []
}
]
}
]
});
if (!victimChar) {
throw new PreconditionError('Victim character not found');
}
if (victimChar.id === performerChar.id) {
throw new PreconditionError('Cannot target yourself');
}
}
if (undergroundType.tr === 'sabotage') {
if (!target) {
throw new PreconditionError('Sabotage target missing');
@@ -6832,23 +6868,67 @@ ORDER BY r.id`,
}
}
// 5) Eintrag anlegen (optional: in Transaction)
const newEntry = await Underground.create({
undergroundTypeId: typeId,
performerId: performerChar.id,
victimId: victimChar.id,
result: {
if (undergroundType.tr === 'raid_transport') {
const parsedRegionId = Number.parseInt(regionId, 10);
const parsedBandSize = Math.max(1, Number.parseInt(bandSize, 10) || 0);
if (!parsedRegionId) {
throw new PreconditionError('Raid region missing');
}
if (!parsedBandSize) {
throw new PreconditionError('Band size missing');
}
const validRegion = await RegionData.findOne({
where: {
id: parsedRegionId,
regionTypeId: { [Op.in]: [4, 5] }
},
include: [
{
model: RegionType,
as: 'regionType',
attributes: ['labelTr']
}
],
attributes: ['id', 'name']
});
if (!validRegion || validRegion.regionType?.labelTr === 'town') {
throw new PreconditionError('Invalid raid region');
}
}
const defaultResult = undergroundType.tr === 'raid_transport'
? {
status: 'pending',
outcome: null,
attempts: 0,
successes: 0,
lastTargetTransportId: null,
lastLoot: null,
lastOutcome: null
}
: {
status: 'pending',
outcome: null,
discoveries: null,
visibilityDelta: 0,
reputationDelta: 0,
blackmailAmount: 0
},
};
const newEntry = await Underground.create({
undergroundTypeId: typeId,
performerId: performerChar.id,
victimId: victimChar?.id || null,
result: defaultResult,
parameters: {
target: target || null,
goal: goal || null,
politicalTargets: politicalTargets || null
politicalTargets: politicalTargets || null,
regionId: regionId || null,
bandSize: bandSize || null
}
});
@@ -6883,6 +6963,19 @@ ORDER BY r.id`,
order: [['createdAt', 'DESC']]
});
const regionIds = [...new Set(
activities
.map((activity) => Number.parseInt(activity.parameters?.regionId, 10))
.filter((id) => !Number.isNaN(id) && id > 0)
)];
const regions = regionIds.length
? await RegionData.findAll({
where: { id: regionIds },
attributes: ['id', 'name']
})
: [];
const regionMap = new Map(regions.map((region) => [region.id, region.name]));
return activities.map((activity) => {
const result = activity.result || {};
const status = result.status || (result.outcome ? 'resolved' : 'pending');
@@ -6896,11 +6989,16 @@ ORDER BY r.id`,
success: result.outcome === 'success',
target: activity.parameters?.target || null,
goal: activity.parameters?.goal || null,
regionName: regionMap.get(Number.parseInt(activity.parameters?.regionId, 10)) || null,
additionalInfo: {
discoveries: result.discoveries || null,
visibilityDelta: result.visibilityDelta ?? null,
reputationDelta: result.reputationDelta ?? null,
blackmailAmount: result.blackmailAmount ?? null
blackmailAmount: result.blackmailAmount ?? null,
bandSize: activity.parameters?.bandSize ?? null,
attempts: result.attempts ?? null,
successes: result.successes ?? null,
lastOutcome: result.lastOutcome ?? null
}
};
});

View File

@@ -0,0 +1,7 @@
-- PostgreSQL only
ALTER TABLE falukant_data.transport
ADD COLUMN IF NOT EXISTS guard_count integer NOT NULL DEFAULT 0;
ALTER TABLE falukant_data.underground
ALTER COLUMN victim_id DROP NOT NULL;

View File

@@ -0,0 +1,5 @@
-- PostgreSQL only
INSERT INTO falukant_type.underground (tr, cost)
VALUES ('raid_transport', 9000)
ON CONFLICT (tr) DO UPDATE
SET cost = EXCLUDED.cost;

View File

@@ -663,6 +663,10 @@ const undergroundTypes = [
"tr": "investigate_affair",
"cost": 7000
},
{
"tr": "raid_transport",
"cost": 9000
},
];
{