Files
corrosion-admin-panel/frontend/src/views/admin/RaidableBasesView.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

1050 lines
45 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRaidableBasesStore } from '@/stores/raidablebases'
import Panel from '@/components/ds/data/Panel.vue'
import Button from '@/components/ds/core/Button.vue'
import Icon from '@/components/ds/core/Icon.vue'
import Switch from '@/components/ds/forms/Switch.vue'
import Tabs from '@/components/ds/navigation/Tabs.vue'
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
const store = useRaidableBasesStore()
const activeTab = ref<'general' | 'difficulty' | 'npc' | 'loot'>('general')
const activeDifficulty = ref<string>('Easy')
const showCreateModal = ref(false)
const showImportModal = ref(false)
const newConfigName = ref('')
const newConfigDesc = ref('')
const importConfigName = ref('')
const tabItems = [
{ value: 'general', label: 'General', icon: 'settings' },
{ value: 'difficulty', label: 'Difficulty', icon: 'shield' },
{ value: 'npc', label: 'NPC settings', icon: 'users' },
{ value: 'loot', label: 'Loot & rewards', icon: 'gift' },
]
const difficultyItems = ['Easy', 'Medium', 'Hard', 'Expert', 'Nightmare'].map(d => ({ value: d, label: d }))
const difficulties = ['Easy', 'Medium', 'Hard', 'Expert', 'Nightmare']
onMounted(async () => {
await store.fetchConfigs()
if (store.configs.length > 0 && store.configs[0]) {
await store.loadConfig(store.configs[0].id)
}
})
// --- Config data helpers ---
function getConfigValue(path: string, defaultValue: any = false): any {
if (!store.currentConfig?.config_data) return defaultValue
const parts = path.split('.')
let current: any = store.currentConfig.config_data
for (const part of parts) {
if (current == null || typeof current !== 'object') return defaultValue
current = current[part]
}
return current ?? defaultValue
}
function setConfigValue(path: string, value: any) {
if (!store.currentConfig) return
if (!store.currentConfig.config_data) store.currentConfig.config_data = {}
const parts = path.split('.')
let current: any = store.currentConfig.config_data
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i]!
if (current[part] == null || typeof current[part] !== 'object') {
current[part] = {}
}
current = current[part]
}
current[parts[parts.length - 1]!] = value
store.markDirty()
}
// --- Action handlers ---
async function handleConfigChange(id: string) {
if (store.isDirty) {
if (!confirm('Unsaved changes will be lost. Continue?')) return
}
await store.loadConfig(id)
}
async function handleCreateConfig() {
if (!newConfigName.value.trim()) return
const config = await store.createConfig(
newConfigName.value.trim(),
newConfigDesc.value.trim() || undefined,
)
if (config) {
showCreateModal.value = false
newConfigName.value = ''
newConfigDesc.value = ''
}
}
async function handleDeleteConfig() {
if (!store.currentConfig) return
if (!confirm(`Delete "${store.currentConfig.config_name}"? This cannot be undone.`)) return
await store.deleteConfig(store.currentConfig.id)
}
async function handleApply() {
if (!store.currentConfig) return
if (!confirm('Apply this RaidableBases config to the server? This will overwrite the current config.')) return
if (store.isDirty) {
await store.saveCurrentConfig()
}
await store.applyToServer(store.currentConfig.id)
}
async function handleImport() {
if (!importConfigName.value.trim()) return
const config = await store.importFromServer(importConfigName.value.trim())
if (config) {
showImportModal.value = false
importConfigName.value = ''
}
}
</script>
<template>
<div class="rb">
<!-- Page head -->
<div class="rb__head">
<div class="rb__head-id">
<div class="rb__head-chip">
<Icon name="swords" :size="20" :stroke-width="2" />
</div>
<div>
<div class="t-eyebrow">Plugin config</div>
<h1 class="rb__title">Raidable bases</h1>
</div>
</div>
<div class="rb__head-actions">
<Button variant="secondary" size="sm" icon="plus" @click="showCreateModal = true">New config</Button>
</div>
</div>
<!-- Config selector + action bar -->
<Panel>
<div class="rb__toolbar">
<select
v-if="store.configs.length > 0"
:value="store.currentConfig?.id ?? ''"
class="rb__config-select"
@change="handleConfigChange(($event.target as HTMLSelectElement).value)"
>
<option v-for="c in store.configs" :key="c.id" :value="c.id">
{{ c.config_name }}{{ c.is_active ? ' (active)' : '' }}
</option>
</select>
<span v-else class="rb__no-configs">No configs yet</span>
<Button
variant="primary"
size="sm"
icon="save"
:loading="store.isSaving"
:disabled="!store.currentConfig || !store.isDirty || store.isSaving"
@click="store.saveCurrentConfig()"
>{{ store.isSaving ? 'Saving…' : 'Save' }}</Button>
<Button
variant="outline"
size="sm"
icon="play"
:disabled="!store.currentConfig || store.isApplying"
@click="handleApply"
>{{ store.isApplying ? 'Applying…' : 'Apply to server' }}</Button>
<Button
variant="secondary"
size="sm"
icon="download"
@click="showImportModal = true"
>Import from server</Button>
<div class="rb__toolbar-spacer" />
<Button
variant="danger-soft"
size="sm"
icon="trash-2"
:disabled="!store.currentConfig"
@click="handleDeleteConfig"
>Delete</Button>
</div>
</Panel>
<!-- Loading state -->
<Panel v-if="store.isLoading">
<div class="rb__loading">
<span class="rb__spinner" />
<span class="rb__loading-label">Loading config</span>
</div>
</Panel>
<!-- Empty state -->
<Panel v-else-if="!store.currentConfig">
<EmptyState
icon="swords"
title="No raidable bases config selected"
description="Create a new config, import from server, or select one from the dropdown above."
>
<template #action>
<Button icon="plus" @click="showCreateModal = true">Create first config</Button>
</template>
</EmptyState>
</Panel>
<!-- Config editor -->
<template v-else>
<!-- Tab bar -->
<Tabs v-model="activeTab" :items="tabItems" variant="line" />
<!-- ======================= GENERAL TAB ======================= -->
<template v-if="activeTab === 'general'">
<!-- Maintained Events -->
<Panel title="Maintained events" subtitle="Keep a set number of raid bases on the map at all times. When one is cleared, a new one spawns.">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Always maintain max events</div>
<div class="rb__toggle-sub">Keep spawning bases to maintain the max count</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Maintained Events.Always Maintain Max Events', false)"
@update:model-value="setConfigValue('Settings.Maintained Events.Always Maintain Max Events', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Include PVE day</div>
<div class="rb__toggle-sub">Spawn PVE bases during daytime</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Maintained Events.Include PVE Day', true)"
@update:model-value="setConfigValue('Settings.Maintained Events.Include PVE Day', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Include PVP day</div>
<div class="rb__toggle-sub">Spawn PVP bases during daytime</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Maintained Events.Include PVP Day', true)"
@update:model-value="setConfigValue('Settings.Maintained Events.Include PVP Day', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Include PVE night</div>
<div class="rb__toggle-sub">Spawn PVE bases during nighttime</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Maintained Events.Include PVE Night', true)"
@update:model-value="setConfigValue('Settings.Maintained Events.Include PVE Night', $event)"
/>
</div>
</div>
<div class="rb__grid3 rb__mt">
<label class="rb__field">
<span class="rb__field-label">Max maintained events</span>
<span class="rb__field-hint">Maximum number of raid bases on map at once</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Maintained Events.Max Maintained Events', 1)"
min="0"
@input="setConfigValue('Settings.Maintained Events.Max Maintained Events', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Min players online</span>
<span class="rb__field-hint">Minimum players required before bases spawn</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Maintained Events.Minimum Required Players Online', 0)"
min="0"
@input="setConfigValue('Settings.Maintained Events.Minimum Required Players Online', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Chance PVP (%)</span>
<span class="rb__field-hint">Chance to randomly spawn PVP bases (0100)</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Maintained Events.Chance To Randomly Spawn PVP Bases', 0)"
min="0"
max="100"
@input="setConfigValue('Settings.Maintained Events.Chance To Randomly Spawn PVP Bases', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
<!-- Scheduled Events -->
<Panel title="Scheduled events" subtitle="Spawn raid bases at random intervals. Configure timing and limits below.">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Enabled</div>
<div class="rb__toggle-sub">Enable scheduled event spawns</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Scheduled Events.Enabled', false)"
@update:model-value="setConfigValue('Settings.Scheduled Events.Enabled', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Include PVP day</div>
<div class="rb__toggle-sub">Spawn PVP scheduled bases during day</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Scheduled Events.Include PVP Day', true)"
@update:model-value="setConfigValue('Settings.Scheduled Events.Include PVP Day', $event)"
/>
</div>
</div>
<div class="rb__grid4 rb__mt">
<label class="rb__field">
<span class="rb__field-label">Every min seconds</span>
<span class="rb__field-hint">Minimum time between spawns</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Scheduled Events.Every Min Seconds', 3600)"
min="0"
@input="setConfigValue('Settings.Scheduled Events.Every Min Seconds', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Every max seconds</span>
<span class="rb__field-hint">Maximum time between spawns</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Scheduled Events.Every Max Seconds', 7200)"
min="0"
@input="setConfigValue('Settings.Scheduled Events.Every Max Seconds', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Max scheduled events</span>
<span class="rb__field-hint">Maximum concurrent scheduled bases</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Scheduled Events.Max Scheduled Events', 1)"
min="0"
@input="setConfigValue('Settings.Scheduled Events.Max Scheduled Events', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Min players online</span>
<span class="rb__field-hint">Required players for scheduled events</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Scheduled Events.Minimum Required Players Online', 0)"
min="0"
@input="setConfigValue('Settings.Scheduled Events.Minimum Required Players Online', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
<!-- General Settings -->
<Panel title="General settings">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Expansion mode</div>
<div class="rb__toggle-sub">Use expansion mode for spawning calculations</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Expansion Mode', false)"
@update:model-value="setConfigValue('Settings.Expansion Mode', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Show X Z coordinates</div>
<div class="rb__toggle-sub">Show base coordinates in chat announcements</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Show X Z Coordinates', false)"
@update:model-value="setConfigValue('Settings.Show X Z Coordinates', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Remove admins from raiders list</div>
<div class="rb__toggle-sub">Exclude admins from the raiders tracking list</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Remove Admins From Raiders List', false)"
@update:model-value="setConfigValue('Settings.Remove Admins From Raiders List', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Block Wizardry plugin at events</div>
<div class="rb__toggle-sub">Prevent Wizardry plugin use during raid events</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Block Wizardry Plugin At Events', false)"
@update:model-value="setConfigValue('Settings.Block Wizardry Plugin At Events', $event)"
/>
</div>
</div>
<div class="rb__grid3 rb__mt">
<label class="rb__field">
<span class="rb__field-label">Entities to undo per batch</span>
<span class="rb__field-hint">Entities cleaned per batch during despawn</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Amount Of Entities To Undo Per Batch', 5)"
min="1"
@input="setConfigValue('Settings.Amount Of Entities To Undo Per Batch', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
<!-- GUI Announcements -->
<Panel title="GUI announcements">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Enabled</div>
<div class="rb__toggle-sub">Show GUI announcement banners for events</div>
</div>
<Switch
:model-value="getConfigValue('GUIAnnouncements.Enabled', false)"
@update:model-value="setConfigValue('GUIAnnouncements.Enabled', $event)"
/>
</div>
</div>
<div class="rb__grid3 rb__mt">
<label class="rb__field">
<span class="rb__field-label">Banner tint color</span>
<span class="rb__field-hint">Color of the announcement banner</span>
<input
type="text"
class="cc-number"
:value="getConfigValue('GUIAnnouncements.Banner Tint Color', 'Grey')"
@input="setConfigValue('GUIAnnouncements.Banner Tint Color', ($event.target as HTMLInputElement).value)"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Text color</span>
<span class="rb__field-hint">Color of the announcement text</span>
<input
type="text"
class="cc-number"
:value="getConfigValue('GUIAnnouncements.Text Color', 'White')"
@input="setConfigValue('GUIAnnouncements.Text Color', ($event.target as HTMLInputElement).value)"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Maximum distance</span>
<span class="rb__field-hint">Max distance to show announcements</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('GUIAnnouncements.Maximum Distance', 300)"
min="0"
@input="setConfigValue('GUIAnnouncements.Maximum Distance', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
</template>
<!-- ======================= DIFFICULTY TAB ======================= -->
<template v-else-if="activeTab === 'difficulty'">
<!-- Difficulty sub-tabs -->
<Panel>
<Tabs v-model="activeDifficulty" :items="difficultyItems" />
</Panel>
<!-- Difficulty settings -->
<Panel :title="activeDifficulty + ' difficulty settings'">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Enabled</div>
<div class="rb__toggle-sub">Allow {{ activeDifficulty.toLowerCase() }} difficulty bases to spawn</div>
</div>
<Switch
:model-value="getConfigValue(`Settings.Raid Management.${activeDifficulty} Raids Can Spawn On.Enabled`, true)"
@update:model-value="setConfigValue(`Settings.Raid Management.${activeDifficulty} Raids Can Spawn On.Enabled`, $event)"
/>
</div>
</div>
<!-- Spawn days -->
<div class="rb__section-head rb__mt">Spawn days</div>
<div class="rb__days">
<div
v-for="day in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']"
:key="day"
class="rb__day"
>
<Switch
:model-value="getConfigValue(`Settings.Raid Management.${activeDifficulty} Raids Can Spawn On.${day}`, true)"
size="sm"
@update:model-value="setConfigValue(`Settings.Raid Management.${activeDifficulty} Raids Can Spawn On.${day}`, $event)"
/>
<span class="rb__day-label">{{ day.slice(0, 3) }}</span>
</div>
</div>
<!-- Map marker colors -->
<div class="rb__section-head rb__mt">Map marker colors</div>
<div class="rb__grid2 rb__mt-sm">
<label class="rb__field">
<span class="rb__field-label">Border color</span>
<span class="rb__field-hint">Map marker border color for {{ activeDifficulty.toLowerCase() }} bases</span>
<input
type="text"
class="cc-number"
:value="getConfigValue(`Settings.Raid Management.${activeDifficulty} Difficulty Colors.Border`, '')"
placeholder="e.g. 00FF00"
@input="setConfigValue(`Settings.Raid Management.${activeDifficulty} Difficulty Colors.Border`, ($event.target as HTMLInputElement).value)"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Inner color</span>
<span class="rb__field-hint">Map marker fill color for {{ activeDifficulty.toLowerCase() }} bases</span>
<input
type="text"
class="cc-number"
:value="getConfigValue(`Settings.Raid Management.${activeDifficulty} Difficulty Colors.Inner`, '')"
placeholder="e.g. 00CC00"
@input="setConfigValue(`Settings.Raid Management.${activeDifficulty} Difficulty Colors.Inner`, ($event.target as HTMLInputElement).value)"
/>
</label>
</div>
</Panel>
<!-- Ranked ladder -->
<Panel title="Ranked ladder">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Enabled</div>
<div class="rb__toggle-sub">Track and display a ranked leaderboard</div>
</div>
<Switch
:model-value="getConfigValue('Ranked Ladder.Enabled', true)"
@update:model-value="setConfigValue('Ranked Ladder.Enabled', $event)"
/>
</div>
</div>
<div class="rb__grid3 rb__mt">
<label class="rb__field">
<span class="rb__field-label">Award top X players on wipe</span>
<span class="rb__field-hint">Number of top players to award on server wipe</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Ranked Ladder.Award Top X Players On Wipe', 3)"
min="0"
@input="setConfigValue('Ranked Ladder.Award Top X Players On Wipe', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
</template>
<!-- ======================= NPC SETTINGS TAB ======================= -->
<template v-else-if="activeTab === 'npc'">
<Panel title="NPC configuration" subtitle="Configure NPC guardians for raid bases. Per-profile NPC settings override these globals when set in profile files.">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Eject mounts</div>
<div class="rb__toggle-sub">Eject players from mounts when entering raid zone</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Raid Management.Eject Mounts', true)"
@update:model-value="setConfigValue('Settings.Raid Management.Eject Mounts', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Reset despawn timer on damage</div>
<div class="rb__toggle-sub">Reset inactive despawn timer when base takes damage</div>
</div>
<Switch
:model-value="getConfigValue('Settings.Raid Management.Despawn Minutes Inactive Reset', false)"
@update:model-value="setConfigValue('Settings.Raid Management.Despawn Minutes Inactive Reset', $event)"
/>
</div>
</div>
<div class="rb__grid3 rb__mt">
<label class="rb__field">
<span class="rb__field-label">Despawn minutes inactive</span>
<span class="rb__field-hint">Minutes before an untouched base despawns</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Raid Management.Despawn Minutes Inactive', 45)"
min="0"
@input="setConfigValue('Settings.Raid Management.Despawn Minutes Inactive', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Max allowed to enter</span>
<span class="rb__field-hint">Max players allowed inside a raid base at once</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Raid Management.Max Amount Allowed To Enter', 0)"
min="0"
@input="setConfigValue('Settings.Raid Management.Max Amount Allowed To Enter', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Max allowed (maintained)</span>
<span class="rb__field-hint">Max players for maintained event bases (0 = unlimited)</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Raid Management.Max Amount Allowed To Enter Maintained', 0)"
min="0"
@input="setConfigValue('Settings.Raid Management.Max Amount Allowed To Enter Maintained', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
<!-- Weapons & Defenses -->
<Panel title="Weapons & defenses">
<div class="rb__grid3">
<label class="rb__field">
<span class="rb__field-label">Turret health</span>
<span class="rb__field-hint">Health of auto turrets in bases</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Weapons.Turret Health', 1000)"
min="0"
@input="setConfigValue('Weapons.Turret Health', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">SAM site range</span>
<span class="rb__field-hint">Range of SAM site defenses (meters)</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Weapons.SamSite Range', 150)"
min="0"
@input="setConfigValue('Weapons.SamSite Range', Number(($event.target as HTMLInputElement).value))"
/>
</label>
<label class="rb__field">
<span class="rb__field-label">Turret starting ammo</span>
<span class="rb__field-hint">Ammo loaded in auto turrets at spawn</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Weapons.Starting Ammo', 256)"
min="0"
@input="setConfigValue('Weapons.Starting Ammo', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
<!-- TruePVE integration -->
<Panel title="TruePVE integration">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Server wide PVP</div>
<div class="rb__toggle-sub">Enable server-wide PVP during raid events</div>
</div>
<Switch
:model-value="getConfigValue('TruePVE.Server Wide PVP', false)"
@update:model-value="setConfigValue('TruePVE.Server Wide PVP', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Allow PVP at events</div>
<div class="rb__toggle-sub">Allow PVP in the raid base area</div>
</div>
<Switch
:model-value="getConfigValue('TruePVE.Allow PVP At Events', true)"
@update:model-value="setConfigValue('TruePVE.Allow PVP At Events', $event)"
/>
</div>
</div>
</Panel>
</template>
<!-- ======================= LOOT & REWARDS TAB ======================= -->
<template v-else-if="activeTab === 'loot'">
<!-- Loot settings -->
<Panel title="Loot settings">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Use random skins</div>
<div class="rb__toggle-sub">Randomize skins on loot items</div>
</div>
<Switch
:model-value="getConfigValue('Treasure.Use Random Skins', false)"
@update:model-value="setConfigValue('Treasure.Use Random Skins', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Include workshop skins</div>
<div class="rb__toggle-sub">Include workshop skins in randomization</div>
</div>
<Switch
:model-value="getConfigValue('Skins.Include Workshop Skins', true)"
@update:model-value="setConfigValue('Skins.Include Workshop Skins', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Randomize NPC item skins</div>
<div class="rb__toggle-sub">Randomize skins on NPC equipment</div>
</div>
<Switch
:model-value="getConfigValue('Skins.Randomize Npc Item Skins', true)"
@update:model-value="setConfigValue('Skins.Randomize Npc Item Skins', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Use day of week loot</div>
<div class="rb__toggle-sub">Vary loot tables based on day of week</div>
</div>
<Switch
:model-value="getConfigValue('Treasure.Use Day Of Week Loot', false)"
@update:model-value="setConfigValue('Treasure.Use Day Of Week Loot', $event)"
/>
</div>
</div>
<div class="rb__grid3 rb__mt">
<label class="rb__field">
<span class="rb__field-label">Items to spawn</span>
<span class="rb__field-hint">Number of loot items per container</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Treasure.Amount Of Items To Spawn', 6)"
min="0"
@input="setConfigValue('Treasure.Amount Of Items To Spawn', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
<!-- Buyable events -->
<Panel title="Buyable events" subtitle="Allow players to purchase raid events using Economics or ServerRewards currency.">
<div class="rb__grid3">
<label class="rb__field">
<span class="rb__field-label">Max buyable events</span>
<span class="rb__field-hint">Maximum concurrent purchased events (0 = disabled)</span>
<input
type="number"
class="cc-number"
:value="getConfigValue('Settings.Buyable Events.Max Buyable Events', 0)"
min="0"
@input="setConfigValue('Settings.Buyable Events.Max Buyable Events', Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
<div class="rb__section-head rb__mt">Economics buy costs</div>
<div class="rb__grid5 rb__mt-sm">
<label v-for="diff in difficulties" :key="'eco-' + diff" class="rb__field">
<span class="rb__field-label">{{ diff }}</span>
<input
type="number"
class="cc-number"
:value="getConfigValue(`Economics Buy Raid Costs.${diff}`, 0)"
min="0"
@input="setConfigValue(`Economics Buy Raid Costs.${diff}`, Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
<div class="rb__section-head rb__mt">ServerRewards buy costs</div>
<div class="rb__grid5 rb__mt-sm">
<label v-for="diff in difficulties" :key="'sr-' + diff" class="rb__field">
<span class="rb__field-label">{{ diff }}</span>
<input
type="number"
class="cc-number"
:value="getConfigValue(`ServerRewards Buy Raid Costs.${diff}`, 0)"
min="0"
@input="setConfigValue(`ServerRewards Buy Raid Costs.${diff}`, Number(($event.target as HTMLInputElement).value))"
/>
</label>
</div>
</Panel>
<!-- UI settings -->
<Panel title="UI settings">
<div class="rb__grid2">
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Buyable UI enabled</div>
<div class="rb__toggle-sub">Show in-game UI for purchasing raid events</div>
</div>
<Switch
:model-value="getConfigValue('UI.Buyable UI.Enabled', false)"
@update:model-value="setConfigValue('UI.Buyable UI.Enabled', $event)"
/>
</div>
<div class="rb__toggle-row">
<div class="rb__toggle-body">
<div class="rb__toggle-label">Lockouts UI enabled</div>
<div class="rb__toggle-sub">Show lockout timer UI for players</div>
</div>
<Switch
:model-value="getConfigValue('UI.Lockouts.Enabled', false)"
@update:model-value="setConfigValue('UI.Lockouts.Enabled', $event)"
/>
</div>
</div>
</Panel>
</template>
</template>
<!-- Create config modal -->
<Teleport to="body">
<div v-if="showCreateModal" class="rb__overlay" @click.self="showCreateModal = false">
<div class="rb__modal">
<div class="rb__modal-head">
<span class="rb__modal-title">New raidable bases config</span>
<button class="rb__modal-close" type="button" @click="showCreateModal = false">
<Icon name="x" :size="16" />
</button>
</div>
<div class="rb__modal-body">
<label class="rb__field">
<span class="rb__field-label">Config name</span>
<input
v-model="newConfigName"
type="text"
class="cc-number"
placeholder="e.g. Default RaidableBases settings"
@keydown.enter="handleCreateConfig"
/>
</label>
<label class="rb__field rb__mt-sm">
<span class="rb__field-label">Description (optional)</span>
<textarea
v-model="newConfigDesc"
rows="2"
class="cc-textarea"
placeholder="What is this config for?"
/>
</label>
</div>
<div class="rb__modal-foot">
<Button variant="ghost" type="button" @click="showCreateModal = false">Cancel</Button>
<Button icon="plus" :disabled="!newConfigName.trim()" @click="handleCreateConfig">Create</Button>
</div>
</div>
</div>
</Teleport>
<!-- Import from server modal -->
<Teleport to="body">
<div v-if="showImportModal" class="rb__overlay" @click.self="showImportModal = false">
<div class="rb__modal">
<div class="rb__modal-head">
<span class="rb__modal-title">Import from server</span>
<button class="rb__modal-close" type="button" @click="showImportModal = false">
<Icon name="x" :size="16" />
</button>
</div>
<div class="rb__modal-body">
<p class="rb__modal-desc">Import the current RaidableBases config from your live server. This will create a new config profile.</p>
<label class="rb__field rb__mt-sm">
<span class="rb__field-label">Config name</span>
<input
v-model="importConfigName"
type="text"
class="cc-number"
placeholder="e.g. Imported server config"
@keydown.enter="handleImport"
/>
</label>
</div>
<div class="rb__modal-foot">
<Button variant="ghost" type="button" @click="showImportModal = false">Cancel</Button>
<Button icon="download" :disabled="!importConfigName.trim()" @click="handleImport">Import</Button>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<style scoped>
/* ---------- Shell ---------- */
.rb { max-width: 900px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
/* ---------- Page head ---------- */
.rb__head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.rb__head-id { display: flex; align-items: center; gap: 12px; }
.rb__head-chip {
width: 40px; height: 40px; flex: none; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
color: var(--accent); background: var(--accent-soft);
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.rb__title {
font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.02em;
color: var(--text-primary); margin-top: 3px;
}
.rb__head-actions { display: flex; align-items: center; gap: 8px; }
/* ---------- Toolbar ---------- */
.rb__toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rb__toolbar-spacer { flex: 1; }
.rb__config-select {
appearance: none; height: var(--control-h-sm); padding: 0 32px 0 11px;
background: var(--surface-inset); color: var(--text-primary); border: 0;
border-radius: var(--radius-sm); box-shadow: var(--ring-default);
font-family: var(--font-sans); font-size: var(--text-sm); cursor: pointer;
min-width: 200px;
}
.rb__config-select:focus { outline: none; box-shadow: inset 0 0 0 1px var(--accent), var(--glow-accent-sm); }
.rb__no-configs { font-size: var(--text-sm); color: var(--text-muted); }
/* ---------- Loading ---------- */
.rb__loading { display: flex; align-items: center; justify-content: center; gap: 10px; padding: 40px; }
.rb__spinner {
width: 20px; height: 20px; border-radius: 50%;
border: 2px solid var(--border-default); border-top-color: var(--accent);
animation: rb-spin 0.7s linear infinite;
}
@keyframes rb-spin { to { transform: rotate(360deg); } }
.rb__loading-label { font-size: var(--text-sm); color: var(--text-tertiary); }
/* ---------- Layout grids ---------- */
.rb__grid2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; }
.rb__grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.rb__grid4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.rb__grid5 { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
@media (max-width: 768px) {
.rb__grid2 { grid-template-columns: 1fr; }
.rb__grid3 { grid-template-columns: repeat(2, 1fr); }
.rb__grid4 { grid-template-columns: repeat(2, 1fr); }
.rb__grid5 { grid-template-columns: repeat(3, 1fr); }
}
/* ---------- Toggle row ---------- */
.rb__toggle-row {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; padding: 13px 0;
border-bottom: 1px solid var(--border-subtle);
}
.rb__grid2 .rb__toggle-row:nth-child(odd) { padding-right: 16px; }
.rb__grid2 .rb__toggle-row:nth-child(even) { padding-left: 16px; }
.rb__toggle-row:last-child, .rb__toggle-row:nth-last-child(2):nth-child(odd) { border-bottom: 0; }
.rb__toggle-row:first-child { padding-top: 0; }
.rb__toggle-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); }
.rb__toggle-sub { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; }
.rb__toggle-body { min-width: 0; }
/* ---------- Field ---------- */
.rb__field { display: flex; flex-direction: column; gap: 4px; }
.rb__field-label { font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); }
.rb__field-hint { font-size: var(--text-xs); color: var(--text-tertiary); }
/* ---------- Section head (inline section label) ---------- */
.rb__section-head {
font-size: var(--text-xs); font-weight: 600; color: var(--text-tertiary);
text-transform: uppercase; letter-spacing: var(--tracking-wider);
}
/* ---------- Spawn days ---------- */
.rb__days { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; }
.rb__day { display: flex; align-items: center; gap: 7px; }
.rb__day-label { font-size: var(--text-xs); color: var(--text-secondary); }
/* ---------- Spacing helpers ---------- */
.rb__mt { margin-top: 16px; }
.rb__mt-sm { margin-top: 10px; }
/* ---------- Number / text input (bare token-styled) ---------- */
.cc-number {
width: 100%; height: var(--control-h-sm); padding: 0 10px;
background: var(--surface-inset); border: 0; border-radius: var(--radius-sm);
box-shadow: var(--ring-default); font-family: var(--font-mono);
font-size: var(--text-sm); color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.cc-number:focus { outline: none; box-shadow: inset 0 0 0 1px var(--accent), var(--glow-accent-sm); }
.cc-number::placeholder { color: var(--text-muted); font-family: var(--font-sans); }
/* ---------- Textarea ---------- */
.cc-textarea {
width: 100%; padding: 8px 10px; min-height: 64px; resize: none;
background: var(--surface-inset); border: 0; border-radius: var(--radius-sm);
box-shadow: var(--ring-default); font-family: var(--font-sans);
font-size: var(--text-sm); color: var(--text-primary); line-height: 1.5;
}
.cc-textarea:focus { outline: none; box-shadow: inset 0 0 0 1px var(--accent), var(--glow-accent-sm); }
.cc-textarea::placeholder { color: var(--text-muted); }
/* ---------- Modal ---------- */
.rb__overlay {
position: fixed; inset: 0; background: color-mix(in srgb, var(--surface-canvas) 50%, transparent);
z-index: 50; display: flex; align-items: center; justify-content: center; padding: 16px;
}
.rb__modal {
background: var(--surface-base); border-radius: var(--radius-lg);
box-shadow: var(--elevation-3); width: 100%; max-width: 440px;
display: flex; flex-direction: column;
}
.rb__modal-head {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 16px; border-bottom: 1px solid var(--border-subtle);
}
.rb__modal-title { font-size: var(--text-base); font-weight: 600; color: var(--text-primary); }
.rb__modal-close {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: var(--radius-sm); border: none;
background: transparent; color: var(--text-tertiary); cursor: pointer;
}
.rb__modal-close:hover { background: var(--surface-hover); color: var(--text-primary); }
.rb__modal-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.rb__modal-desc { font-size: var(--text-sm); color: var(--text-tertiary); line-height: 1.5; }
.rb__modal-foot {
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
padding: 12px 16px; border-top: 1px solid var(--border-subtle);
}
</style>