diff --git a/backend-nest/src/app.module.ts b/backend-nest/src/app.module.ts index 9c7bb0f..16a964c 100644 --- a/backend-nest/src/app.module.ts +++ b/backend-nest/src/app.module.ts @@ -35,6 +35,7 @@ import { SetupModule } from './modules/setup/setup.module'; import { MigrationModule } from './modules/migration/migration.module'; import { ChangelogModule } from './modules/changelog/changelog.module'; import { FilesModule } from './modules/files/files.module'; +import { LootModule } from './modules/loot/loot.module'; // Shared Services import { NatsService } from './services/nats.service'; @@ -105,6 +106,7 @@ import { NatsBridgeGateway } from './gateways/nats-bridge.gateway'; MigrationModule, ChangelogModule, FilesModule, + LootModule, ], providers: [ // Global guards (order matters: auth first, then license, then permissions) diff --git a/backend-nest/src/entities/loot-profile.entity.ts b/backend-nest/src/entities/loot-profile.entity.ts new file mode 100644 index 0000000..161c1fb --- /dev/null +++ b/backend-nest/src/entities/loot-profile.entity.ts @@ -0,0 +1,36 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { License } from './license.entity'; + +@Entity('loot_profiles') +export class LootProfile { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + license_id: string; + + @Column({ type: 'varchar', length: 100 }) + profile_name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'jsonb', default: () => "'{}'" }) + loot_table: Record; + + @Column({ type: 'jsonb', default: () => "'{}'" }) + loot_groups: Record; + + @Column({ type: 'boolean', default: false }) + is_active: boolean; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + created_at: Date; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + updated_at: Date; + + @ManyToOne(() => License, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'license_id' }) + license: License; +} diff --git a/backend-nest/src/modules/loot/data/rust-containers.ts b/backend-nest/src/modules/loot/data/rust-containers.ts new file mode 100644 index 0000000..bcc3703 --- /dev/null +++ b/backend-nest/src/modules/loot/data/rust-containers.ts @@ -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' }, +]; diff --git a/backend-nest/src/modules/loot/dto/apply-loot-profile.dto.ts b/backend-nest/src/modules/loot/dto/apply-loot-profile.dto.ts new file mode 100644 index 0000000..0f87fe5 --- /dev/null +++ b/backend-nest/src/modules/loot/dto/apply-loot-profile.dto.ts @@ -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; +} diff --git a/backend-nest/src/modules/loot/dto/create-loot-profile.dto.ts b/backend-nest/src/modules/loot/dto/create-loot-profile.dto.ts new file mode 100644 index 0000000..863b660 --- /dev/null +++ b/backend-nest/src/modules/loot/dto/create-loot-profile.dto.ts @@ -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; + + @ApiPropertyOptional() + @IsObject() + @IsOptional() + loot_groups?: Record; +} diff --git a/backend-nest/src/modules/loot/dto/import-loot-profile.dto.ts b/backend-nest/src/modules/loot/dto/import-loot-profile.dto.ts new file mode 100644 index 0000000..77a87a3 --- /dev/null +++ b/backend-nest/src/modules/loot/dto/import-loot-profile.dto.ts @@ -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; + + @ApiPropertyOptional({ description: 'BetterLoot LootGroups.json content' }) + @IsObject() + @IsOptional() + loot_groups?: Record; +} diff --git a/backend-nest/src/modules/loot/dto/update-loot-profile.dto.ts b/backend-nest/src/modules/loot/dto/update-loot-profile.dto.ts new file mode 100644 index 0000000..d8b713d --- /dev/null +++ b/backend-nest/src/modules/loot/dto/update-loot-profile.dto.ts @@ -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; + + @ApiPropertyOptional() + @IsObject() + @IsOptional() + loot_groups?: Record; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + is_active?: boolean; +} diff --git a/backend-nest/src/modules/loot/loot.controller.ts b/backend-nest/src/modules/loot/loot.controller.ts new file mode 100644 index 0000000..d4feab8 --- /dev/null +++ b/backend-nest/src/modules/loot/loot.controller.ts @@ -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(); + } +} diff --git a/backend-nest/src/modules/loot/loot.module.ts b/backend-nest/src/modules/loot/loot.module.ts new file mode 100644 index 0000000..4a8affc --- /dev/null +++ b/backend-nest/src/modules/loot/loot.module.ts @@ -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 {} diff --git a/backend-nest/src/modules/loot/loot.service.ts b/backend-nest/src/modules/loot/loot.service.ts new file mode 100644 index 0000000..ce043b5 --- /dev/null +++ b/backend-nest/src/modules/loot/loot.service.ts @@ -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, + 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, 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, 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, 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, field: string, multiplier: number) { + if (typeof obj[field] === 'number') { + obj[field] = Math.round(obj[field] * multiplier); + } + } +} diff --git a/backend/migrations/013_loot_profiles.sql b/backend/migrations/013_loot_profiles.sql new file mode 100644 index 0000000..4b33615 --- /dev/null +++ b/backend/migrations/013_loot_profiles.sql @@ -0,0 +1,13 @@ +-- Loot profiles for BetterLoot integration +CREATE TABLE loot_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + license_id UUID NOT NULL REFERENCES licenses(id) ON DELETE CASCADE, + profile_name VARCHAR(100) NOT NULL, + description TEXT, + loot_table JSONB NOT NULL DEFAULT '{}', + loot_groups JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_loot_profiles_license ON loot_profiles(license_id); diff --git a/frontend/src/data/rust-containers.ts b/frontend/src/data/rust-containers.ts new file mode 100644 index 0000000..d8ec9a8 --- /dev/null +++ b/frontend/src/data/rust-containers.ts @@ -0,0 +1,72 @@ +export interface RustContainer { + prefab: string + name: string + category: 'crates' | 'barrels' | 'military' | 'npcs' | 'other' +} + +export const rustContainers: RustContainer[] = [ + // 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_fuel.prefab', name: 'Fuel 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' }, + { prefab: 'assets/bundled/prefabs/radtown/vehicle_parts.prefab', name: 'Vehicle Parts', 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' }, + { prefab: 'assets/bundled/prefabs/radtown/loot-barrel-1.prefab', name: 'Barrel (Alt)', category: 'barrels' }, + { prefab: 'assets/bundled/prefabs/radtown/loot-barrel-2.prefab', name: 'Barrel 2 (Alt)', 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' }, + { prefab: 'assets/bundled/prefabs/radtown/crate_normal_2_food.prefab', name: 'Military Food Crate', category: 'military' }, + { prefab: 'assets/bundled/prefabs/radtown/crate_normal_2_medical.prefab', name: 'Military Medical 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_patrol.prefab', name: 'Patrol Scientist', category: 'npcs' }, + { prefab: 'assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_junkpile.prefab', name: 'Junkpile Scientist', category: 'npcs' }, + { prefab: 'assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_peacekeeper.prefab', name: 'Peacekeeper', 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/scientist/scientistnpc_ch47_gunner.prefab', name: 'Chinook Gunner', category: 'npcs' }, + { prefab: 'assets/rust.ai/agents/npcplayer/humannpc/tunneldweller/npc_tunneldweller.prefab', name: 'Tunnel Dweller', category: 'npcs' }, + { prefab: 'assets/rust.ai/agents/npcplayer/humannpc/underwaterdweller/npc_underwaterdweller.prefab', name: 'Underwater Dweller', category: 'npcs' }, + { prefab: 'assets/rust.ai/agents/npcplayer/humannpc/scarecrow/scarecrow.prefab', name: 'Scarecrow', category: 'npcs' }, + + // Other + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm ammo.prefab', name: 'DM Ammo', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm c4.prefab', name: 'DM C4', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm construction resources.prefab', name: 'DM Construction', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm construction tools.prefab', name: 'DM Tools', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm food.prefab', name: 'DM Food', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm medical.prefab', name: 'DM Medical', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm res.prefab', name: 'DM Resources', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm tier1 lootbox.prefab', name: 'DM Tier 1 Lootbox', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm tier2 lootbox.prefab', name: 'DM Tier 2 Lootbox', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/dmloot/dm tier3 lootbox.prefab', name: 'DM Tier 3 Lootbox', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/minecart.prefab', name: 'Minecart', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/underwater_labs/crate_food_1_underwater_lab.prefab', name: 'Lab Food Crate', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/underwater_labs/crate_normal_underwater_lab.prefab', name: 'Lab Crate', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/underwater_labs/crate_elite_underwater_lab.prefab', name: 'Lab Elite Crate', category: 'other' }, + { prefab: 'assets/bundled/prefabs/radtown/underwater_labs/crate_tools_underwater_lab.prefab', name: 'Lab Tool Crate', category: 'other' }, +] + +export const containerCategories = ['crates', 'barrels', 'military', 'npcs', 'other'] as const + +export type ContainerCategory = typeof containerCategories[number] diff --git a/frontend/src/data/rust-items.ts b/frontend/src/data/rust-items.ts new file mode 100644 index 0000000..5f7bacc --- /dev/null +++ b/frontend/src/data/rust-items.ts @@ -0,0 +1,259 @@ +export interface RustItem { + shortname: string + name: string + category: 'weapons' | 'ammo' | 'medical' | 'attire' | 'tools' | 'resources' | 'components' | 'food' | 'traps' | 'construction' | 'electrical' | 'fun' | 'misc' + maxStack: number +} + +export const rustItems: RustItem[] = [ + // Weapons + { shortname: 'rifle.ak', name: 'Assault Rifle', category: 'weapons', maxStack: 1 }, + { shortname: 'rifle.lr300', name: 'LR-300', category: 'weapons', maxStack: 1 }, + { shortname: 'rifle.bolt', name: 'Bolt Action Rifle', category: 'weapons', maxStack: 1 }, + { shortname: 'rifle.m39', name: 'M39 Rifle', category: 'weapons', maxStack: 1 }, + { shortname: 'rifle.semiauto', name: 'Semi-Auto Rifle', category: 'weapons', maxStack: 1 }, + { shortname: 'rifle.l96', name: 'L96 Rifle', category: 'weapons', maxStack: 1 }, + { shortname: 'smg.mp5', name: 'MP5A4', category: 'weapons', maxStack: 1 }, + { shortname: 'smg.thompson', name: 'Thompson', category: 'weapons', maxStack: 1 }, + { shortname: 'smg.2', name: 'Custom SMG', category: 'weapons', maxStack: 1 }, + { shortname: 'pistol.revolver', name: 'Revolver', category: 'weapons', maxStack: 1 }, + { shortname: 'pistol.semiauto', name: 'Semi-Auto Pistol', category: 'weapons', maxStack: 1 }, + { shortname: 'pistol.python', name: 'Python Revolver', category: 'weapons', maxStack: 1 }, + { shortname: 'pistol.m92', name: 'M92 Pistol', category: 'weapons', maxStack: 1 }, + { shortname: 'pistol.nailgun', name: 'Nailgun', category: 'weapons', maxStack: 1 }, + { shortname: 'shotgun.pump', name: 'Pump Shotgun', category: 'weapons', maxStack: 1 }, + { shortname: 'shotgun.spas12', name: 'Spas-12', category: 'weapons', maxStack: 1 }, + { shortname: 'shotgun.double', name: 'Double Barrel Shotgun', category: 'weapons', maxStack: 1 }, + { shortname: 'shotgun.waterpipe', name: 'Waterpipe Shotgun', category: 'weapons', maxStack: 1 }, + { shortname: 'lmg.m249', name: 'M249', category: 'weapons', maxStack: 1 }, + { shortname: 'rocket.launcher', name: 'Rocket Launcher', category: 'weapons', maxStack: 1 }, + { shortname: 'multiplegrenadelauncher', name: 'Multiple Grenade Launcher', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.lasersight', name: 'Laser Sight', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.holosight', name: 'Holosight', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.flashlight', name: 'Flashlight', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.silencer', name: 'Silencer', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.simplesight', name: 'Simple Handmade Sight', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.small.scope', name: 'Handmade Scope', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.8x.scope', name: '8x Zoom Scope', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.muzzleboost', name: 'Muzzle Boost', category: 'weapons', maxStack: 1 }, + { shortname: 'weapon.mod.muzzlebrake', name: 'Muzzle Brake', category: 'weapons', maxStack: 1 }, + { shortname: 'crossbow', name: 'Crossbow', category: 'weapons', maxStack: 1 }, + { shortname: 'bow.hunting', name: 'Hunting Bow', category: 'weapons', maxStack: 1 }, + { shortname: 'bow.compound', name: 'Compound Bow', category: 'weapons', maxStack: 1 }, + { shortname: 'spear.wooden', name: 'Wooden Spear', category: 'weapons', maxStack: 1 }, + { shortname: 'spear.stone', name: 'Stone Spear', category: 'weapons', maxStack: 1 }, + { shortname: 'machete', name: 'Machete', category: 'weapons', maxStack: 1 }, + { shortname: 'longsword', name: 'Longsword', category: 'weapons', maxStack: 1 }, + { shortname: 'salvaged.sword', name: 'Salvaged Sword', category: 'weapons', maxStack: 1 }, + { shortname: 'salvaged.cleaver', name: 'Salvaged Cleaver', category: 'weapons', maxStack: 1 }, + { shortname: 'knife.combat', name: 'Combat Knife', category: 'weapons', maxStack: 1 }, + { shortname: 'bone.club', name: 'Bone Club', category: 'weapons', maxStack: 1 }, + { shortname: 'mace', name: 'Mace', category: 'weapons', maxStack: 1 }, + { shortname: 'grenade.f1', name: 'F1 Grenade', category: 'weapons', maxStack: 1 }, + { shortname: 'grenade.beancan', name: 'Beancan Grenade', category: 'weapons', maxStack: 1 }, + { shortname: 'explosive.satchel', name: 'Satchel Charge', category: 'weapons', maxStack: 1 }, + { shortname: 'explosive.timed', name: 'Timed Explosive', category: 'weapons', maxStack: 1 }, + { shortname: 'surveycharge', name: 'Survey Charge', category: 'weapons', maxStack: 1 }, + { shortname: 'flare', name: 'Flare', category: 'weapons', maxStack: 1 }, + { shortname: 'pistol.eoka', name: 'Eoka Pistol', category: 'weapons', maxStack: 1 }, + + // Ammo + { shortname: 'ammo.rifle', name: '5.56 Rifle Ammo', category: 'ammo', maxStack: 128 }, + { shortname: 'ammo.rifle.hv', name: 'HV 5.56 Rifle Ammo', category: 'ammo', maxStack: 128 }, + { shortname: 'ammo.rifle.incendiary', name: 'Incendiary 5.56 Ammo', category: 'ammo', maxStack: 128 }, + { shortname: 'ammo.rifle.explosive', name: 'Explosive 5.56 Ammo', category: 'ammo', maxStack: 128 }, + { shortname: 'ammo.pistol', name: 'Pistol Bullet', category: 'ammo', maxStack: 128 }, + { shortname: 'ammo.pistol.hv', name: 'HV Pistol Ammo', category: 'ammo', maxStack: 128 }, + { shortname: 'ammo.pistol.fire', name: 'Incendiary Pistol Bullet', category: 'ammo', maxStack: 128 }, + { shortname: 'ammo.shotgun', name: 'Handmade Shell', category: 'ammo', maxStack: 64 }, + { shortname: 'ammo.shotgun.slug', name: '12 Gauge Slug', category: 'ammo', maxStack: 64 }, + { shortname: 'ammo.shotgun.fire', name: '12 Gauge Incendiary Shell', category: 'ammo', maxStack: 64 }, + { shortname: 'ammo.rocket.basic', name: 'Rocket', category: 'ammo', maxStack: 3 }, + { shortname: 'ammo.rocket.hv', name: 'HV Rocket', category: 'ammo', maxStack: 3 }, + { shortname: 'ammo.rocket.fire', name: 'Incendiary Rocket', category: 'ammo', maxStack: 3 }, + { shortname: 'arrow.wooden', name: 'Wooden Arrow', category: 'ammo', maxStack: 64 }, + { shortname: 'arrow.hv', name: 'High Velocity Arrow', category: 'ammo', maxStack: 64 }, + { shortname: 'arrow.fire', name: 'Fire Arrow', category: 'ammo', maxStack: 64 }, + { shortname: 'arrow.bone', name: 'Bone Arrow', category: 'ammo', maxStack: 64 }, + { shortname: 'ammo.nailgun.nails', name: 'Nailgun Nails', category: 'ammo', maxStack: 64 }, + { shortname: 'ammo.grenadelauncher.he', name: '40mm HE Grenade', category: 'ammo', maxStack: 12 }, + { shortname: 'ammo.grenadelauncher.smoke', name: '40mm Smoke Grenade', category: 'ammo', maxStack: 12 }, + + // Medical + { shortname: 'syringe.medical', name: 'Medical Syringe', category: 'medical', maxStack: 3 }, + { shortname: 'largemedkit', name: 'Large Medkit', category: 'medical', maxStack: 1 }, + { shortname: 'bandage', name: 'Bandage', category: 'medical', maxStack: 3 }, + { shortname: 'antiradpills', name: 'Anti-Radiation Pills', category: 'medical', maxStack: 10 }, + { shortname: 'blood', name: 'Blood', category: 'medical', maxStack: 1 }, + + // Attire + { shortname: 'metal.facemask', name: 'Metal Facemask', category: 'attire', maxStack: 1 }, + { shortname: 'metal.plate.torso', name: 'Metal Chest Plate', category: 'attire', maxStack: 1 }, + { shortname: 'roadsign.jacket', name: 'Roadsign Jacket', category: 'attire', maxStack: 1 }, + { shortname: 'roadsign.kilt', name: 'Roadsign Kilt', category: 'attire', maxStack: 1 }, + { shortname: 'coffeecan.helmet', name: 'Coffee Can Helmet', category: 'attire', maxStack: 1 }, + { shortname: 'riot.helmet', name: 'Riot Helmet', category: 'attire', maxStack: 1 }, + { shortname: 'bucket.helmet', name: 'Bucket Helmet', category: 'attire', maxStack: 1 }, + { shortname: 'hoodie', name: 'Hoodie', category: 'attire', maxStack: 1 }, + { shortname: 'pants', name: 'Pants', category: 'attire', maxStack: 1 }, + { shortname: 'shoes.boots', name: 'Boots', category: 'attire', maxStack: 1 }, + { shortname: 'burlap.shirt', name: 'Burlap Shirt', category: 'attire', maxStack: 1 }, + { shortname: 'burlap.trousers', name: 'Burlap Trousers', category: 'attire', maxStack: 1 }, + { shortname: 'burlap.shoes', name: 'Burlap Shoes', category: 'attire', maxStack: 1 }, + { shortname: 'burlap.headwrap', name: 'Burlap Headwrap', category: 'attire', maxStack: 1 }, + { shortname: 'burlap.gloves', name: 'Burlap Gloves', category: 'attire', maxStack: 1 }, + { shortname: 'hat.wolf', name: 'Wolf Headdress', category: 'attire', maxStack: 1 }, + { shortname: 'hat.boonie', name: 'Boonie Hat', category: 'attire', maxStack: 1 }, + { shortname: 'hat.beenie', name: 'Beenie Hat', category: 'attire', maxStack: 1 }, + { shortname: 'hat.miner', name: 'Miners Hat', category: 'attire', maxStack: 1 }, + { shortname: 'hat.candle', name: 'Candle Hat', category: 'attire', maxStack: 1 }, + { shortname: 'attire.hide.poncho', name: 'Hide Poncho', category: 'attire', maxStack: 1 }, + { shortname: 'attire.hide.vest', name: 'Hide Vest', category: 'attire', maxStack: 1 }, + { shortname: 'attire.hide.boots', name: 'Hide Boots', category: 'attire', maxStack: 1 }, + { shortname: 'attire.hide.pants', name: 'Hide Pants', category: 'attire', maxStack: 1 }, + { shortname: 'attire.hide.skirt', name: 'Hide Skirt', category: 'attire', maxStack: 1 }, + { shortname: 'deer.skull.mask', name: 'Deer Skull Mask', category: 'attire', maxStack: 1 }, + { shortname: 'bone.armor.suit', name: 'Bone Armor', category: 'attire', maxStack: 1 }, + { shortname: 'heavy.plate.helmet', name: 'Heavy Plate Helmet', category: 'attire', maxStack: 1 }, + { shortname: 'heavy.plate.jacket', name: 'Heavy Plate Jacket', category: 'attire', maxStack: 1 }, + { shortname: 'heavy.plate.pants', name: 'Heavy Plate Pants', category: 'attire', maxStack: 1 }, + { shortname: 'hazmatsuit', name: 'Hazmat Suit', category: 'attire', maxStack: 1 }, + { shortname: 'nightvisiongoggles', name: 'Night Vision Goggles', category: 'attire', maxStack: 1 }, + { shortname: 'tactical.gloves', name: 'Tactical Gloves', category: 'attire', maxStack: 1 }, + + // Tools + { shortname: 'hatchet', name: 'Hatchet', category: 'tools', maxStack: 1 }, + { shortname: 'pickaxe', name: 'Pickaxe', category: 'tools', maxStack: 1 }, + { shortname: 'stone.pickaxe', name: 'Stone Pickaxe', category: 'tools', maxStack: 1 }, + { shortname: 'stonehatchet', name: 'Stone Hatchet', category: 'tools', maxStack: 1 }, + { shortname: 'rock', name: 'Rock', category: 'tools', maxStack: 1 }, + { shortname: 'torch', name: 'Torch', category: 'tools', maxStack: 1 }, + { shortname: 'jackhammer', name: 'Jackhammer', category: 'tools', maxStack: 1 }, + { shortname: 'chainsaw', name: 'Chainsaw', category: 'tools', maxStack: 1 }, + { shortname: 'hammer', name: 'Hammer', category: 'tools', maxStack: 1 }, + { shortname: 'wire.cutter', name: 'Wire Cutter', category: 'tools', maxStack: 1 }, + { shortname: 'tool.binoculars', name: 'Binoculars', category: 'tools', maxStack: 1 }, + { shortname: 'tool.camera', name: 'Camera', category: 'tools', maxStack: 1 }, + { shortname: 'geiger.counter', name: 'Geiger Counter', category: 'tools', maxStack: 1 }, + { shortname: 'supply.signal', name: 'Supply Signal', category: 'tools', maxStack: 1 }, + { shortname: 'map', name: 'Map', category: 'tools', maxStack: 1 }, + { shortname: 'note', name: 'Note', category: 'tools', maxStack: 1 }, + { shortname: 'blueprintbase', name: 'Blueprint', category: 'tools', maxStack: 1 }, + + // Resources + { shortname: 'wood', name: 'Wood', category: 'resources', maxStack: 1000 }, + { shortname: 'stones', name: 'Stones', category: 'resources', maxStack: 1000 }, + { shortname: 'metal.ore', name: 'Metal Ore', category: 'resources', maxStack: 1000 }, + { shortname: 'metal.fragments', name: 'Metal Fragments', category: 'resources', maxStack: 1000 }, + { shortname: 'metal.refined', name: 'High Quality Metal', category: 'resources', maxStack: 100 }, + { shortname: 'sulfur.ore', name: 'Sulfur Ore', category: 'resources', maxStack: 1000 }, + { shortname: 'sulfur', name: 'Sulfur', category: 'resources', maxStack: 1000 }, + { shortname: 'gunpowder', name: 'Gun Powder', category: 'resources', maxStack: 500 }, + { shortname: 'explosives', name: 'Explosives', category: 'resources', maxStack: 10 }, + { shortname: 'charcoal', name: 'Charcoal', category: 'resources', maxStack: 1000 }, + { shortname: 'lowgradefuel', name: 'Low Grade Fuel', category: 'resources', maxStack: 500 }, + { shortname: 'crude.oil', name: 'Crude Oil', category: 'resources', maxStack: 500 }, + { shortname: 'leather', name: 'Leather', category: 'resources', maxStack: 1000 }, + { shortname: 'cloth', name: 'Cloth', category: 'resources', maxStack: 1000 }, + { shortname: 'fat.animal', name: 'Animal Fat', category: 'resources', maxStack: 1000 }, + { shortname: 'bone.fragments', name: 'Bone Fragments', category: 'resources', maxStack: 1000 }, + { shortname: 'scrap', name: 'Scrap', category: 'resources', maxStack: 1000 }, + { shortname: 'diesel_barrel', name: 'Diesel Fuel', category: 'resources', maxStack: 20 }, + + // Components + { shortname: 'riflebody', name: 'Rifle Body', category: 'components', maxStack: 1 }, + { shortname: 'smgbody', name: 'SMG Body', category: 'components', maxStack: 1 }, + { shortname: 'semibody', name: 'Semi Auto Body', category: 'components', maxStack: 1 }, + { shortname: 'metalpipe', name: 'Metal Pipe', category: 'components', maxStack: 5 }, + { shortname: 'metalspring', name: 'Metal Spring', category: 'components', maxStack: 5 }, + { shortname: 'gears', name: 'Gears', category: 'components', maxStack: 5 }, + { shortname: 'roadsigns', name: 'Road Signs', category: 'components', maxStack: 5 }, + { shortname: 'sewingkit', name: 'Sewing Kit', category: 'components', maxStack: 5 }, + { shortname: 'tarp', name: 'Tarp', category: 'components', maxStack: 5 }, + { shortname: 'rope', name: 'Rope', category: 'components', maxStack: 5 }, + { shortname: 'sheetmetal', name: 'Sheet Metal', category: 'components', maxStack: 5 }, + { shortname: 'techparts', name: 'Tech Trash', category: 'components', maxStack: 5 }, + { shortname: 'propanetank', name: 'Propane Tank', category: 'components', maxStack: 5 }, + { shortname: 'targeting.computer', name: 'Targeting Computer', category: 'components', maxStack: 1 }, + { shortname: 'cctv.camera', name: 'CCTV Camera', category: 'components', maxStack: 1 }, + { shortname: 'electric.fuse', name: 'Fuse', category: 'components', maxStack: 5 }, + { shortname: 'bleach', name: 'Bleach', category: 'components', maxStack: 5 }, + { shortname: 'ducttape', name: 'Duct Tape', category: 'components', maxStack: 5 }, + + // Food + { shortname: 'apple', name: 'Apple', category: 'food', maxStack: 10 }, + { shortname: 'granolabar', name: 'Granola Bar', category: 'food', maxStack: 10 }, + { shortname: 'can.beans', name: 'Can of Beans', category: 'food', maxStack: 10 }, + { shortname: 'can.tuna', name: 'Can of Tuna', category: 'food', maxStack: 10 }, + { shortname: 'chocbar', name: 'Chocolate Bar', category: 'food', maxStack: 10 }, + { shortname: 'mushroom', name: 'Mushroom', category: 'food', maxStack: 20 }, + { shortname: 'meat.boar', name: 'Boar Meat', category: 'food', maxStack: 20 }, + { shortname: 'chicken.raw', name: 'Raw Chicken', category: 'food', maxStack: 20 }, + { shortname: 'humanmeat.raw', name: 'Raw Human Meat', category: 'food', maxStack: 20 }, + { shortname: 'wolfmeat.raw', name: 'Raw Wolf Meat', category: 'food', maxStack: 20 }, + { shortname: 'deermeat.raw', name: 'Raw Deer Meat', category: 'food', maxStack: 20 }, + { shortname: 'bearmeat', name: 'Bear Meat', category: 'food', maxStack: 20 }, + { shortname: 'fish.raw', name: 'Raw Fish', category: 'food', maxStack: 20 }, + { shortname: 'corn', name: 'Corn', category: 'food', maxStack: 20 }, + { shortname: 'pumpkin', name: 'Pumpkin', category: 'food', maxStack: 20 }, + { shortname: 'potato', name: 'Potato', category: 'food', maxStack: 20 }, + { shortname: 'waterjug', name: 'Water Jug', category: 'food', maxStack: 1 }, + { shortname: 'water', name: 'Water', category: 'food', maxStack: 1 }, + { shortname: 'water.purified', name: 'Pure Water', category: 'food', maxStack: 10 }, + + // Traps + { shortname: 'trap.bear', name: 'Snap Trap', category: 'traps', maxStack: 3 }, + { shortname: 'trap.landmine', name: 'Landmine', category: 'traps', maxStack: 3 }, + { shortname: 'autoturret', name: 'Auto Turret', category: 'traps', maxStack: 1 }, + { shortname: 'flameturret', name: 'Flame Turret', category: 'traps', maxStack: 1 }, + { shortname: 'guntrap', name: 'Shotgun Trap', category: 'traps', maxStack: 1 }, + { shortname: 'sam.site', name: 'SAM Site', category: 'traps', maxStack: 1 }, + + // Construction + { shortname: 'wall.external.high', name: 'High External Wall', category: 'construction', maxStack: 10 }, + { shortname: 'wall.external.high.stone', name: 'High External Stone Wall', category: 'construction', maxStack: 10 }, + { shortname: 'gates.external.high.wood', name: 'High External Wooden Gate', category: 'construction', maxStack: 1 }, + { shortname: 'gates.external.high.stone', name: 'High External Stone Gate', category: 'construction', maxStack: 1 }, + { shortname: 'barricade.metal', name: 'Metal Barricade', category: 'construction', maxStack: 3 }, + { shortname: 'barricade.sandbags', name: 'Sandbag Barricade', category: 'construction', maxStack: 5 }, + { shortname: 'barricade.concrete', name: 'Concrete Barricade', category: 'construction', maxStack: 3 }, + { shortname: 'barricade.wood', name: 'Wooden Barricade', category: 'construction', maxStack: 5 }, + { shortname: 'barricade.woodwire', name: 'Barbed Wooden Barricade', category: 'construction', maxStack: 3 }, + { shortname: 'lock.code', name: 'Code Lock', category: 'construction', maxStack: 1 }, + { shortname: 'lock.key', name: 'Key Lock', category: 'construction', maxStack: 1 }, + + // Misc + { shortname: 'workbench1', name: 'Work Bench Level 1', category: 'misc', maxStack: 1 }, + { shortname: 'workbench2', name: 'Work Bench Level 2', category: 'misc', maxStack: 1 }, + { shortname: 'workbench3', name: 'Work Bench Level 3', category: 'misc', maxStack: 1 }, + { shortname: 'furnace', name: 'Furnace', category: 'misc', maxStack: 1 }, + { shortname: 'furnace.large', name: 'Large Furnace', category: 'misc', maxStack: 1 }, + { shortname: 'campfire', name: 'Camp Fire', category: 'misc', maxStack: 1 }, + { shortname: 'box.wooden', name: 'Wood Storage Box', category: 'misc', maxStack: 1 }, + { shortname: 'box.wooden.large', name: 'Large Wood Box', category: 'misc', maxStack: 1 }, + { shortname: 'cupboard.tool', name: 'Tool Cupboard', category: 'misc', maxStack: 1 }, + { shortname: 'sleepingbag', name: 'Sleeping Bag', category: 'misc', maxStack: 1 }, + { shortname: 'bed', name: 'Bed', category: 'misc', maxStack: 1 }, + { shortname: 'research.table', name: 'Research Table', category: 'misc', maxStack: 1 }, + { shortname: 'mining.quarry', name: 'Mining Quarry', category: 'misc', maxStack: 1 }, + { shortname: 'small.oil.refinery', name: 'Small Oil Refinery', category: 'misc', maxStack: 1 }, + { shortname: 'water.purifier', name: 'Water Purifier', category: 'misc', maxStack: 1 }, + { shortname: 'stocking.small', name: 'Small Stocking', category: 'misc', maxStack: 1 }, + { shortname: 'stocking.large', name: 'Large Stocking', category: 'misc', maxStack: 1 }, + { shortname: 'kayak', name: 'Kayak', category: 'misc', maxStack: 1 }, + { shortname: 'fridge', name: 'Fridge', category: 'misc', maxStack: 1 }, + { shortname: 'locker', name: 'Locker', category: 'misc', maxStack: 1 }, + { shortname: 'vending.machine', name: 'Vending Machine', category: 'misc', maxStack: 1 }, + { shortname: 'wall.frame.shopfront', name: 'Shop Front', category: 'misc', maxStack: 1 }, + { shortname: 'door.hinged.metal', name: 'Sheet Metal Door', category: 'misc', maxStack: 1 }, + { shortname: 'door.hinged.toptier', name: 'Armored Door', category: 'misc', maxStack: 1 }, + { shortname: 'door.double.hinged.metal', name: 'Sheet Metal Double Door', category: 'misc', maxStack: 1 }, + { shortname: 'door.double.hinged.toptier', name: 'Armored Double Door', category: 'misc', maxStack: 1 }, +] + +export const itemCategories = [ + 'weapons', 'ammo', 'medical', 'attire', 'tools', 'resources', + 'components', 'food', 'traps', 'construction', 'electrical', 'fun', 'misc', +] as const + +export type ItemCategory = typeof itemCategories[number] diff --git a/frontend/src/stores/loot.ts b/frontend/src/stores/loot.ts index 405000f..06fd021 100644 --- a/frontend/src/stores/loot.ts +++ b/frontend/src/stores/loot.ts @@ -38,7 +38,7 @@ export const useLootStore = defineStore('loot', () => { // Select first container if none selected if (!selectedContainer.value && currentProfile.value.loot_table) { const keys = Object.keys(currentProfile.value.loot_table) - if (keys.length > 0) selectedContainer.value = keys[0] + if (keys.length > 0) selectedContainer.value = keys[0] ?? null } } catch (err) { toast.error((err as Error).message) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 5efd6ca..4691e83 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -442,3 +442,80 @@ export interface DeploymentStatus { message: string error?: string } + +// Loot Builder types — BetterLoot integration +export interface LootProfileSummary { + id: string + profile_name: string + description: string | null + is_active: boolean + created_at: string + updated_at: string +} + +export interface LootProfileFull { + id: string + license_id: string + profile_name: string + description: string | null + loot_table: Record + loot_groups: Record + is_active: boolean + created_at: string + updated_at: string +} + +export interface PrefabLoot { + Enabled: boolean + LootProfiles: LootProfileRef[] + GuaranteedItems: Record + UngroupedItems: Record + ItemSettings: { + ItemsMin: number + ItemsMax: number + MinScrap: number + MaxScrap: number + } +} + +export interface LootProfileRef { + Enabled: boolean + LootProfileName: string + LootProfileProbability: number +} + +export interface LootItemSettings { + Min: number + Max: number + SkinId: number + DisplayName: string +} + +export interface LootEntry extends LootItemSettings { + DurabilitySettings: { + MinDurability: number + MaxDurability: number + } + ItemEntryModifications: { + AmmoSettings: Record | null + AttachmentSettings: Record | null + } + BonusItems: Record +} + +export interface LootRNG extends LootEntry { + Probability: number +} + +export interface LootGroupProfile { + Enabled: boolean + GuaranteedItems: Record + ItemList: Record +} + +export interface LootApplyResult { + success: boolean + message: string + profile_name: string + multiplier: number +} diff --git a/frontend/src/views/admin/LootBuilderView.vue b/frontend/src/views/admin/LootBuilderView.vue index 4f343dc..0ec95c9 100644 --- a/frontend/src/views/admin/LootBuilderView.vue +++ b/frontend/src/views/admin/LootBuilderView.vue @@ -26,7 +26,7 @@ const multipliers = [1, 2, 5, 10] onMounted(async () => { await loot.fetchProfiles() - if (loot.profiles.length > 0) { + if (loot.profiles.length > 0 && loot.profiles[0]) { await loot.loadProfile(loot.profiles[0].id) } }) @@ -110,7 +110,7 @@ function handleAddItem(shortname: string) { ItemSettings: { ItemsMin: 1, ItemsMax: 6, MinScrap: 0, MaxScrap: 0 }, } } - const container = table[loot.selectedContainer] + const container = table[loot.selectedContainer]! if (!container.UngroupedItems) container.UngroupedItems = {} if (!container.UngroupedItems[shortname]) { container.UngroupedItems[shortname] = {