feat: Add loot builder backend + static data + DB migration
- Migration 013: loot_profiles table (JSONB loot_table + loot_groups, license-scoped) - TypeORM entity matching migration schema exactly - NestJS loot module: 10 endpoints (CRUD, duplicate, apply, import, export, containers) - Multiplier logic recursively scales Min/Max/Scrap across loot tables and groups - Apply-to-server writes BetterLoot JSON via NATS file manager + RCON reload - Frontend static data: 191 Rust items, 51 container prefabs - TypeScript types for BetterLoot data model (PrefabLoot, LootEntry, LootRNG, etc.) - Fix vue-tsc errors: UngroupedItems uses LootRNG, null safety in store/view Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
39
backend-nest/src/modules/loot/data/rust-containers.ts
Normal file
39
backend-nest/src/modules/loot/data/rust-containers.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export interface RustContainerInfo {
|
||||
prefab: string;
|
||||
name: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export const RUST_CONTAINERS: RustContainerInfo[] = [
|
||||
// Crates
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_normal.prefab', name: 'Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_normal_2.prefab', name: 'Crate (Small)', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_elite.prefab', name: 'Elite Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_tools.prefab', name: 'Tool Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_food_1.prefab', name: 'Food Crate (Small)', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_food_2.prefab', name: 'Food Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_medical.prefab', name: 'Medical Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_mine.prefab', name: 'Mine Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_basic.prefab', name: 'Basic Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_underwater_basic.prefab', name: 'Underwater Basic Crate', category: 'crates' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_underwater_advanced.prefab', name: 'Underwater Advanced Crate', category: 'crates' },
|
||||
// Barrels
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/loot_barrel_1.prefab', name: 'Barrel', category: 'barrels' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/loot_barrel_2.prefab', name: 'Barrel 2', category: 'barrels' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/oil_barrel.prefab', name: 'Oil Barrel', category: 'barrels' },
|
||||
// Military
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_helicopter.prefab', name: 'Helicopter Crate', category: 'military' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/bradley_crate.prefab', name: 'Bradley Crate', category: 'military' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/supply_drop.prefab', name: 'Supply Drop', category: 'military' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_locked.prefab', name: 'Locked Crate', category: 'military' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/crate_hackable.prefab', name: 'Hackable Crate', category: 'military' },
|
||||
// NPCs
|
||||
{ prefab: 'assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_any.prefab', name: 'Scientist', category: 'npcs' },
|
||||
{ prefab: 'assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_heavy.prefab', name: 'Heavy Scientist', category: 'npcs' },
|
||||
{ prefab: 'assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_oilrig.prefab', name: 'Oil Rig Scientist', category: 'npcs' },
|
||||
{ prefab: 'assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_cargo.prefab', name: 'Cargo Ship Scientist', category: 'npcs' },
|
||||
{ prefab: 'assets/rust.ai/agents/npcplayer/humannpc/tunneldweller/npc_tunneldweller.prefab', name: 'Tunnel Dweller', category: 'npcs' },
|
||||
// Other
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/minecart.prefab', name: 'Minecart', category: 'other' },
|
||||
{ prefab: 'assets/bundled/prefabs/radtown/vehicle_parts.prefab', name: 'Vehicle Parts', category: 'other' },
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IsNumber, IsIn } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ApplyLootProfileDto {
|
||||
@ApiProperty({ example: 1, description: 'Loot multiplier', enum: [1, 2, 5, 10] })
|
||||
@IsNumber()
|
||||
@IsIn([1, 2, 5, 10])
|
||||
multiplier: number;
|
||||
}
|
||||
24
backend-nest/src/modules/loot/dto/create-loot-profile.dto.ts
Normal file
24
backend-nest/src/modules/loot/dto/create-loot-profile.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsString, IsOptional, IsObject, MaxLength } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateLootProfileDto {
|
||||
@ApiProperty({ example: 'Vanilla 2x' })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
profile_name: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Standard 2x loot table' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
loot_table?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
loot_groups?: Record<string, any>;
|
||||
}
|
||||
23
backend-nest/src/modules/loot/dto/import-loot-profile.dto.ts
Normal file
23
backend-nest/src/modules/loot/dto/import-loot-profile.dto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IsString, IsObject, IsOptional, MaxLength } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class ImportLootProfileDto {
|
||||
@ApiProperty({ example: 'Imported from Looty' })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
profile_name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: 'BetterLoot LootTables.json content' })
|
||||
@IsObject()
|
||||
loot_table: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'BetterLoot LootGroups.json content' })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
loot_groups?: Record<string, any>;
|
||||
}
|
||||
30
backend-nest/src/modules/loot/dto/update-loot-profile.dto.ts
Normal file
30
backend-nest/src/modules/loot/dto/update-loot-profile.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IsString, IsOptional, IsObject, IsBoolean, MaxLength } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateLootProfileDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
@IsOptional()
|
||||
profile_name?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
loot_table?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
loot_groups?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
is_active?: boolean;
|
||||
}
|
||||
112
backend-nest/src/modules/loot/loot.controller.ts
Normal file
112
backend-nest/src/modules/loot/loot.controller.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { LootService } from './loot.service';
|
||||
import { CreateLootProfileDto } from './dto/create-loot-profile.dto';
|
||||
import { UpdateLootProfileDto } from './dto/update-loot-profile.dto';
|
||||
import { ApplyLootProfileDto } from './dto/apply-loot-profile.dto';
|
||||
import { ImportLootProfileDto } from './dto/import-loot-profile.dto';
|
||||
import { CurrentTenant } from '../../common/decorators/current-tenant.decorator';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../../common/guards/permissions.guard';
|
||||
|
||||
@ApiTags('loot')
|
||||
@ApiBearerAuth()
|
||||
@Controller('loot')
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class LootController {
|
||||
constructor(private readonly lootService: LootService) {}
|
||||
|
||||
@Get('profiles')
|
||||
@RequirePermission('loot.view')
|
||||
@ApiOperation({ summary: 'List loot profiles (summaries)' })
|
||||
getProfiles(@CurrentTenant() licenseId: string) {
|
||||
return this.lootService.getProfiles(licenseId);
|
||||
}
|
||||
|
||||
@Get('profiles/:id')
|
||||
@RequirePermission('loot.view')
|
||||
@ApiOperation({ summary: 'Get full loot profile with data' })
|
||||
getProfile(@CurrentTenant() licenseId: string, @Param('id') id: string) {
|
||||
return this.lootService.getProfile(licenseId, id);
|
||||
}
|
||||
|
||||
@Post('profiles')
|
||||
@RequirePermission('loot.manage')
|
||||
@ApiOperation({ summary: 'Create loot profile' })
|
||||
createProfile(@CurrentTenant() licenseId: string, @Body() dto: CreateLootProfileDto) {
|
||||
return this.lootService.createProfile(licenseId, dto);
|
||||
}
|
||||
|
||||
@Put('profiles/:id')
|
||||
@RequirePermission('loot.manage')
|
||||
@ApiOperation({ summary: 'Update loot profile' })
|
||||
updateProfile(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateLootProfileDto,
|
||||
) {
|
||||
return this.lootService.updateProfile(licenseId, id, dto);
|
||||
}
|
||||
|
||||
@Delete('profiles/:id')
|
||||
@RequirePermission('loot.manage')
|
||||
@ApiOperation({ summary: 'Delete loot profile' })
|
||||
deleteProfile(@CurrentTenant() licenseId: string, @Param('id') id: string) {
|
||||
return this.lootService.deleteProfile(licenseId, id);
|
||||
}
|
||||
|
||||
@Post('profiles/:id/duplicate')
|
||||
@RequirePermission('loot.manage')
|
||||
@ApiOperation({ summary: 'Duplicate loot profile' })
|
||||
duplicateProfile(@CurrentTenant() licenseId: string, @Param('id') id: string) {
|
||||
return this.lootService.duplicateProfile(licenseId, id);
|
||||
}
|
||||
|
||||
@Post('profiles/:id/apply')
|
||||
@RequirePermission('loot.manage')
|
||||
@ApiOperation({ summary: 'Apply loot profile to server with multiplier' })
|
||||
applyToServer(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ApplyLootProfileDto,
|
||||
) {
|
||||
return this.lootService.applyToServer(licenseId, id, dto.multiplier);
|
||||
}
|
||||
|
||||
@Post('import')
|
||||
@RequirePermission('loot.manage')
|
||||
@ApiOperation({ summary: 'Import BetterLoot/Looty JSON as new profile' })
|
||||
importProfile(@CurrentTenant() licenseId: string, @Body() dto: ImportLootProfileDto) {
|
||||
return this.lootService.importProfile(licenseId, dto);
|
||||
}
|
||||
|
||||
@Get('export/:id')
|
||||
@RequirePermission('loot.view')
|
||||
@ApiOperation({ summary: 'Export loot profile as BetterLoot JSON' })
|
||||
@ApiQuery({ name: 'multiplier', required: false, example: 1 })
|
||||
exportProfile(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Param('id') id: string,
|
||||
@Query('multiplier') multiplier: string,
|
||||
) {
|
||||
return this.lootService.exportProfile(licenseId, id, multiplier ? parseInt(multiplier, 10) : 1);
|
||||
}
|
||||
|
||||
@Get('containers')
|
||||
@RequirePermission('loot.view')
|
||||
@ApiOperation({ summary: 'Get list of Rust container prefabs' })
|
||||
getContainers() {
|
||||
return this.lootService.getContainers();
|
||||
}
|
||||
}
|
||||
14
backend-nest/src/modules/loot/loot.module.ts
Normal file
14
backend-nest/src/modules/loot/loot.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { LootController } from './loot.controller';
|
||||
import { LootService } from './loot.service';
|
||||
import { LootProfile } from '../../entities/loot-profile.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([LootProfile])],
|
||||
controllers: [LootController],
|
||||
providers: [LootService, NatsService],
|
||||
exports: [LootService],
|
||||
})
|
||||
export class LootModule {}
|
||||
258
backend-nest/src/modules/loot/loot.service.ts
Normal file
258
backend-nest/src/modules/loot/loot.service.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { LootProfile } from '../../entities/loot-profile.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { CreateLootProfileDto } from './dto/create-loot-profile.dto';
|
||||
import { UpdateLootProfileDto } from './dto/update-loot-profile.dto';
|
||||
import { ImportLootProfileDto } from './dto/import-loot-profile.dto';
|
||||
import { RUST_CONTAINERS } from './data/rust-containers';
|
||||
|
||||
@Injectable()
|
||||
export class LootService {
|
||||
private readonly logger = new Logger(LootService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(LootProfile)
|
||||
private readonly lootRepo: Repository<LootProfile>,
|
||||
private readonly natsService: NatsService,
|
||||
) {}
|
||||
|
||||
/** List profiles for a license (summaries — no JSONB) */
|
||||
async getProfiles(licenseId: string) {
|
||||
const profiles = await this.lootRepo.find({
|
||||
where: { license_id: licenseId },
|
||||
select: ['id', 'profile_name', 'description', 'is_active', 'created_at', 'updated_at'],
|
||||
order: { created_at: 'DESC' },
|
||||
});
|
||||
return { profiles };
|
||||
}
|
||||
|
||||
/** Get full profile with JSONB data */
|
||||
async getProfile(licenseId: string, profileId: string) {
|
||||
const profile = await this.lootRepo.findOne({
|
||||
where: { id: profileId, license_id: licenseId },
|
||||
});
|
||||
if (!profile) throw new NotFoundException('Loot profile not found');
|
||||
return { profile };
|
||||
}
|
||||
|
||||
/** Create a new profile */
|
||||
async createProfile(licenseId: string, dto: CreateLootProfileDto) {
|
||||
const profile = this.lootRepo.create({
|
||||
license_id: licenseId,
|
||||
profile_name: dto.profile_name,
|
||||
description: dto.description || null,
|
||||
loot_table: dto.loot_table || {},
|
||||
loot_groups: dto.loot_groups || {},
|
||||
});
|
||||
const saved = await this.lootRepo.save(profile);
|
||||
return { profile: saved };
|
||||
}
|
||||
|
||||
/** Update an existing profile */
|
||||
async updateProfile(licenseId: string, profileId: string, dto: UpdateLootProfileDto) {
|
||||
const profile = await this.lootRepo.findOne({
|
||||
where: { id: profileId, license_id: licenseId },
|
||||
});
|
||||
if (!profile) throw new NotFoundException('Loot profile not found');
|
||||
|
||||
if (dto.profile_name !== undefined) profile.profile_name = dto.profile_name;
|
||||
if (dto.description !== undefined) profile.description = dto.description;
|
||||
if (dto.loot_table !== undefined) profile.loot_table = dto.loot_table;
|
||||
if (dto.loot_groups !== undefined) profile.loot_groups = dto.loot_groups;
|
||||
if (dto.is_active !== undefined) profile.is_active = dto.is_active;
|
||||
profile.updated_at = new Date();
|
||||
|
||||
const saved = await this.lootRepo.save(profile);
|
||||
return { profile: saved };
|
||||
}
|
||||
|
||||
/** Delete a profile */
|
||||
async deleteProfile(licenseId: string, profileId: string) {
|
||||
const result = await this.lootRepo.delete({ id: profileId, license_id: licenseId });
|
||||
if (result.affected === 0) throw new NotFoundException('Loot profile not found');
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
/** Duplicate a profile */
|
||||
async duplicateProfile(licenseId: string, profileId: string) {
|
||||
const source = await this.lootRepo.findOne({
|
||||
where: { id: profileId, license_id: licenseId },
|
||||
});
|
||||
if (!source) throw new NotFoundException('Loot profile not found');
|
||||
|
||||
const copy = this.lootRepo.create({
|
||||
license_id: licenseId,
|
||||
profile_name: `${source.profile_name} (Copy)`,
|
||||
description: source.description,
|
||||
loot_table: JSON.parse(JSON.stringify(source.loot_table)),
|
||||
loot_groups: JSON.parse(JSON.stringify(source.loot_groups)),
|
||||
is_active: false,
|
||||
});
|
||||
const saved = await this.lootRepo.save(copy);
|
||||
return { profile: saved };
|
||||
}
|
||||
|
||||
/** Apply profile to server with multiplier */
|
||||
async applyToServer(licenseId: string, profileId: string, multiplier: number) {
|
||||
const profile = await this.lootRepo.findOne({
|
||||
where: { id: profileId, license_id: licenseId },
|
||||
});
|
||||
if (!profile) throw new NotFoundException('Loot profile not found');
|
||||
|
||||
// Deep clone and apply multiplier
|
||||
const scaledTable = JSON.parse(JSON.stringify(profile.loot_table));
|
||||
const scaledGroups = JSON.parse(JSON.stringify(profile.loot_groups));
|
||||
|
||||
if (multiplier !== 1) {
|
||||
this.applyMultiplierToTable(scaledTable, multiplier);
|
||||
this.applyMultiplierToGroups(scaledGroups, multiplier);
|
||||
}
|
||||
|
||||
const lootTablesJson = JSON.stringify(scaledTable, null, 2);
|
||||
const lootGroupsJson = JSON.stringify(scaledGroups, null, 2);
|
||||
|
||||
try {
|
||||
// Write LootTables.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/data/BetterLoot/LootTables.json',
|
||||
content: lootTablesJson,
|
||||
},
|
||||
30000,
|
||||
);
|
||||
|
||||
// Write LootGroups.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/data/BetterLoot/LootGroups.json',
|
||||
content: lootGroupsJson,
|
||||
},
|
||||
30000,
|
||||
);
|
||||
|
||||
// Reload BetterLoot plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload BetterLoot',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
|
||||
// Mark this profile as active, deactivate others
|
||||
await this.lootRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
await this.lootRepo.update(
|
||||
{ id: profileId, license_id: licenseId },
|
||||
{ is_active: true, updated_at: new Date() },
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Profile "${profile.profile_name}" applied with ${multiplier}x multiplier`,
|
||||
profile_name: profile.profile_name,
|
||||
multiplier,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to apply loot profile: ${(error as Error).message}`);
|
||||
throw new HttpException(
|
||||
'Failed to apply loot profile — agent may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Import BetterLoot/Looty JSON as a new profile */
|
||||
async importProfile(licenseId: string, dto: ImportLootProfileDto) {
|
||||
const profile = this.lootRepo.create({
|
||||
license_id: licenseId,
|
||||
profile_name: dto.profile_name,
|
||||
description: dto.description || 'Imported profile',
|
||||
loot_table: dto.loot_table,
|
||||
loot_groups: dto.loot_groups || {},
|
||||
});
|
||||
const saved = await this.lootRepo.save(profile);
|
||||
return { profile: saved };
|
||||
}
|
||||
|
||||
/** Export profile as BetterLoot-compatible JSON with optional multiplier */
|
||||
async exportProfile(licenseId: string, profileId: string, multiplier: number) {
|
||||
const profile = await this.lootRepo.findOne({
|
||||
where: { id: profileId, license_id: licenseId },
|
||||
});
|
||||
if (!profile) throw new NotFoundException('Loot profile not found');
|
||||
|
||||
const exportTable = JSON.parse(JSON.stringify(profile.loot_table));
|
||||
const exportGroups = JSON.parse(JSON.stringify(profile.loot_groups));
|
||||
|
||||
if (multiplier && multiplier !== 1) {
|
||||
this.applyMultiplierToTable(exportTable, multiplier);
|
||||
this.applyMultiplierToGroups(exportGroups, multiplier);
|
||||
}
|
||||
|
||||
return {
|
||||
profile_name: profile.profile_name,
|
||||
multiplier: multiplier || 1,
|
||||
loot_table: exportTable,
|
||||
loot_groups: exportGroups,
|
||||
};
|
||||
}
|
||||
|
||||
/** Get static list of Rust container prefabs */
|
||||
getContainers() {
|
||||
return { containers: RUST_CONTAINERS };
|
||||
}
|
||||
|
||||
// --- Multiplier helpers ---
|
||||
|
||||
private applyMultiplierToTable(table: Record<string, any>, multiplier: number) {
|
||||
for (const prefab of Object.values(table)) {
|
||||
if (prefab?.ItemSettings) {
|
||||
this.scaleField(prefab.ItemSettings, 'ItemsMin', multiplier);
|
||||
this.scaleField(prefab.ItemSettings, 'ItemsMax', multiplier);
|
||||
this.scaleField(prefab.ItemSettings, 'MinScrap', multiplier);
|
||||
this.scaleField(prefab.ItemSettings, 'MaxScrap', multiplier);
|
||||
}
|
||||
if (prefab?.GuaranteedItems) {
|
||||
this.scaleItems(prefab.GuaranteedItems, multiplier);
|
||||
}
|
||||
if (prefab?.UngroupedItems) {
|
||||
this.scaleItems(prefab.UngroupedItems, multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyMultiplierToGroups(groups: Record<string, any>, multiplier: number) {
|
||||
for (const group of Object.values(groups)) {
|
||||
if (group?.GuaranteedItems) {
|
||||
this.scaleItems(group.GuaranteedItems, multiplier);
|
||||
}
|
||||
if (group?.ItemList) {
|
||||
this.scaleItems(group.ItemList, multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scaleItems(items: Record<string, any>, multiplier: number) {
|
||||
for (const item of Object.values(items)) {
|
||||
this.scaleField(item, 'Min', multiplier);
|
||||
this.scaleField(item, 'Max', multiplier);
|
||||
// Recursively scale bonus items
|
||||
if (item?.BonusItems) {
|
||||
this.scaleItems(item.BonusItems, multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scaleField(obj: Record<string, any>, field: string, multiplier: number) {
|
||||
if (typeof obj[field] === 'number') {
|
||||
obj[field] = Math.round(obj[field] * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user