Files
corrosion-admin-panel/frontend/src/components/loot/LootItemEditor.vue
Vantz Stockwell 376ed9a98d
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
feat(redesign): re-skin plugin-config editors + Loot Builder to DS (Phase D batch 3)
10 plugin-config views (LootBuilder, RaidableBases, Teleport, Kits, Gather, AutoDoors, FurnaceSplitter, BetterChat, TimedExecute, PluginConfigs landing) + 5 child components (loot sidebar/item-editor/group-editor/item-picker, teleport PermissionGroupEditor) re-skinned onto DS components + tokens. All config logic preserved (path-traversal get/set, apply-to-server, import-from-server, CRUD, multiplier logic, per-store status derivation). Presentation-only. Build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:46:16 -04:00

354 lines
9.7 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { rustItems } from '@/data/rust-items'
import { rustContainers } from '@/data/rust-containers'
import Panel from '@/components/ds/data/Panel.vue'
import Button from '@/components/ds/core/Button.vue'
import IconButton from '@/components/ds/core/IconButton.vue'
import Badge from '@/components/ds/core/Badge.vue'
import Switch from '@/components/ds/forms/Switch.vue'
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
import type { PrefabLoot } from '@/types'
const props = defineProps<{
containerKey: string
lootTable: Record<string, any>
}>()
const emit = defineEmits<{
dirty: []
'add-item': []
}>()
const containerName = computed(() => {
const c = rustContainers.find(c => c.prefab === props.containerKey)
return c?.name || props.containerKey.split('/').pop()?.replace('.prefab', '') || 'Unknown'
})
const containerData = computed<PrefabLoot | null>(() => {
return props.lootTable[props.containerKey] || null
})
function ensureContainer() {
if (!props.lootTable[props.containerKey]) {
props.lootTable[props.containerKey] = {
Enabled: true,
LootProfiles: [],
GuaranteedItems: {},
UngroupedItems: {},
ItemSettings: { ItemsMin: 1, ItemsMax: 6, MinScrap: 0, MaxScrap: 0 },
}
emit('dirty')
}
}
function getItemName(shortname: string): string {
return rustItems.find(i => i.shortname === shortname)?.name || shortname
}
function updateItemField(shortname: string, field: string, value: number) {
ensureContainer()
const items = props.lootTable[props.containerKey].UngroupedItems
if (items[shortname]) {
items[shortname][field] = value
emit('dirty')
}
}
function updateSettings(field: string, value: number) {
ensureContainer()
props.lootTable[props.containerKey].ItemSettings[field] = value
emit('dirty')
}
function toggleEnabled() {
ensureContainer()
props.lootTable[props.containerKey].Enabled = !props.lootTable[props.containerKey].Enabled
emit('dirty')
}
function removeItem(shortname: string) {
if (!containerData.value?.UngroupedItems) return
delete props.lootTable[props.containerKey].UngroupedItems[shortname]
emit('dirty')
}
const ungroupedItems = computed(() => {
if (!containerData.value?.UngroupedItems) return []
return Object.entries(containerData.value.UngroupedItems).map(([shortname, data]) => ({
shortname,
name: getItemName(shortname),
...(data as any),
}))
})
// Computed boolean for the Switch v-model
const isEnabled = computed({
get: () => containerData.value?.Enabled ?? true,
set: () => toggleEnabled(),
})
</script>
<template>
<div class="lie-root">
<!-- Container settings panel -->
<Panel :title="containerName">
<template #actions>
<Badge tone="neutral" mono class="lie-prefab">
{{ containerKey.split('/').pop() }}
</Badge>
<Switch v-model="isEnabled" label="Enabled" size="sm" />
</template>
<!-- Item settings grid -->
<div v-if="containerData" class="lie-settings">
<div class="lie-setting">
<label class="lie-setting__label">Items min</label>
<input
type="number"
:value="containerData.ItemSettings?.ItemsMin ?? 1"
@input="updateSettings('ItemsMin', Number(($event.target as HTMLInputElement).value))"
class="cc-num-input"
min="0"
/>
</div>
<div class="lie-setting">
<label class="lie-setting__label">Items max</label>
<input
type="number"
:value="containerData.ItemSettings?.ItemsMax ?? 6"
@input="updateSettings('ItemsMax', Number(($event.target as HTMLInputElement).value))"
class="cc-num-input"
min="0"
/>
</div>
<div class="lie-setting">
<label class="lie-setting__label">Min scrap</label>
<input
type="number"
:value="containerData.ItemSettings?.MinScrap ?? 0"
@input="updateSettings('MinScrap', Number(($event.target as HTMLInputElement).value))"
class="cc-num-input"
min="0"
/>
</div>
<div class="lie-setting">
<label class="lie-setting__label">Max scrap</label>
<input
type="number"
:value="containerData.ItemSettings?.MaxScrap ?? 0"
@input="updateSettings('MaxScrap', Number(($event.target as HTMLInputElement).value))"
class="cc-num-input"
min="0"
/>
</div>
</div>
<p v-else class="lie-unconfigured">
Container not yet configured. Add an item to initialise its settings.
</p>
</Panel>
<!-- Ungrouped items panel -->
<Panel title="Ungrouped items" :flush-body="ungroupedItems.length > 0">
<template #actions>
<Button size="sm" variant="outline" icon="plus" @click="emit('add-item')">
Add item
</Button>
</template>
<div v-if="ungroupedItems.length > 0" class="lie-table-wrap">
<table class="lie-table">
<thead>
<tr>
<th class="lie-th">Item</th>
<th class="lie-th lie-th--num">Min</th>
<th class="lie-th lie-th--num">Max</th>
<th class="lie-th lie-th--num">Prob %</th>
<th class="lie-th lie-th--action"></th>
</tr>
</thead>
<tbody>
<tr
v-for="item in ungroupedItems"
:key="item.shortname"
class="lie-tr"
>
<td class="lie-td">
<span class="lie-item-name">{{ item.name }}</span>
<span class="lie-item-short">{{ item.shortname }}</span>
</td>
<td class="lie-td lie-td--num">
<input
type="number"
:value="item.Min"
@input="updateItemField(item.shortname, 'Min', Number(($event.target as HTMLInputElement).value))"
class="cc-num-input cc-num-input--center"
min="0"
/>
</td>
<td class="lie-td lie-td--num">
<input
type="number"
:value="item.Max"
@input="updateItemField(item.shortname, 'Max', Number(($event.target as HTMLInputElement).value))"
class="cc-num-input cc-num-input--center"
min="0"
/>
</td>
<td class="lie-td lie-td--num">
<input
type="number"
:value="item.Probability ?? 100"
@input="updateItemField(item.shortname, 'Probability', Number(($event.target as HTMLInputElement).value))"
class="cc-num-input cc-num-input--center"
min="0"
max="100"
/>
</td>
<td class="lie-td lie-td--action">
<IconButton
icon="trash-2"
variant="danger"
size="sm"
label="Remove item"
@click="removeItem(item.shortname)"
/>
</td>
</tr>
</tbody>
</table>
</div>
<EmptyState
v-else
icon="package"
title="No items configured"
description="Add items to configure what this container can spawn."
>
<template #action>
<Button size="sm" variant="outline" icon="plus" @click="emit('add-item')">
Add item
</Button>
</template>
</EmptyState>
</Panel>
</div>
</template>
<style scoped>
.lie-root {
display: flex;
flex-direction: column;
gap: 14px;
}
/* Badge for prefab key */
.lie-prefab {
font-size: 10px !important;
}
.lie-unconfigured {
font-size: var(--text-sm);
color: var(--text-tertiary);
}
/* Settings grid */
.lie-settings {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.lie-setting {
display: flex;
flex-direction: column;
gap: 5px;
}
.lie-setting__label {
font-size: var(--text-xs);
color: var(--text-tertiary);
font-weight: 500;
}
/* Table */
.lie-table-wrap {
overflow-x: auto;
}
.lie-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}
.lie-th {
padding: 8px 12px;
text-align: left;
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-tertiary);
border-bottom: 1px solid var(--border-subtle);
white-space: nowrap;
}
.lie-th--num {
text-align: center;
width: 80px;
}
.lie-th--action {
width: 40px;
}
.lie-tr {
border-bottom: 1px solid var(--border-subtle);
transition: var(--transition-colors);
}
.lie-tr:last-child {
border-bottom: 0;
}
.lie-tr:hover {
background: var(--surface-hover);
}
.lie-td {
padding: 8px 12px;
color: var(--text-primary);
vertical-align: middle;
}
.lie-td--num {
text-align: center;
width: 80px;
}
.lie-td--action {
width: 40px;
padding: 4px 8px;
}
.lie-item-name {
display: block;
color: var(--text-primary);
}
.lie-item-short {
display: block;
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--text-muted);
margin-top: 1px;
}
/* Shared number input */
.cc-num-input {
width: 100%;
background: var(--surface-inset);
border: 0;
border-radius: var(--radius-sm);
box-shadow: var(--ring-default);
padding: 5px 8px;
font-family: var(--font-mono);
font-size: var(--text-sm);
font-variant-numeric: tabular-nums;
color: var(--text-primary);
outline: none;
transition: var(--transition-colors);
}
.cc-num-input:focus {
box-shadow: inset 0 0 0 1px var(--accent), var(--glow-accent-sm);
}
.cc-num-input--center {
text-align: center;
}
</style>