feat(07-04): integrate RuntimeVariableDisplay into InventoryPanel and QuestPanel

- InventoryPanel: runtime vars in all 3 item sections (equipped, backpack, world)
  with display and edit modes for each
- QuestPanel: runtime vars in both active and history beat sections
- Both panels load definitions via database.getRuntimeVariablesByEntityType
- All four entity panels now have identical runtime variable integration
This commit is contained in:
munimunigamer 2026-02-17 23:36:18 -06:00
parent 7e111651d9
commit 08d9147a00
2 changed files with 229 additions and 0 deletions

View file

@ -14,6 +14,7 @@
X,
} from 'lucide-svelte'
import type { Item } from '$lib/types'
import type { RuntimeVariable, RuntimeVarsMap } from '$lib/services/packs/types'
import { Button } from '$lib/components/ui/button'
import { Input } from '$lib/components/ui/input'
import { Textarea } from '$lib/components/ui/textarea'
@ -23,6 +24,8 @@
import * as Select from '$lib/components/ui/select'
import IconRow from '$lib/components/ui/icon-row.svelte'
import { cn } from '$lib/utils/cn'
import { database } from '$lib/services/database'
import RuntimeVariableDisplay from './RuntimeVariableDisplay.svelte'
let showAddForm = $state(false)
let newName = $state('')
@ -36,6 +39,41 @@
let droppingItemId = $state<string | null>(null)
let dropLocationId = $state<string>('')
// Runtime variables
let runtimeVarDefs = $state<RuntimeVariable[]>([])
let editRuntimeVars = $state<RuntimeVarsMap>({})
$effect(() => {
if (story.currentStory) {
loadRuntimeVarDefs()
}
})
async function loadRuntimeVarDefs() {
if (!story.currentStory) return
try {
const packId = await database.getStoryPackId(story.currentStory.id)
if (packId) {
runtimeVarDefs = await database.getRuntimeVariablesByEntityType(packId, 'item')
} else {
runtimeVarDefs = []
}
} catch {
runtimeVarDefs = []
}
}
function updateEditRuntimeVar(
defId: string,
variableName: string,
value: string | number | null,
) {
editRuntimeVars = {
...editRuntimeVars,
[defId]: { variableName, v: value },
}
}
const worldItems = $derived(story.items.filter((item) => item.location !== 'inventory'))
function toggleCollapse(itemId: string) {
@ -60,6 +98,9 @@
editEquipped = item.equipped
// Reset other modes
droppingItemId = null
// Initialize runtime vars from entity metadata
const rv = (item.metadata as Record<string, unknown> | null)?.runtimeVars
editRuntimeVars = rv && typeof rv === 'object' ? { ...(rv as RuntimeVarsMap) } : {}
}
function cancelEdit() {
@ -68,6 +109,7 @@
editDescription = ''
editQuantity = 1
editEquipped = false
editRuntimeVars = {}
}
async function saveEdit(item: Item) {
@ -75,11 +117,26 @@
if (!name) return
const quantity = Math.max(1, Number(editQuantity) || 1)
// Merge runtime vars into metadata
const existingMeta = (item.metadata as Record<string, unknown>) ?? {}
const hasRuntimeVarEdits = Object.keys(editRuntimeVars).length > 0
const updatedMetadata = hasRuntimeVarEdits
? {
...existingMeta,
runtimeVars: {
...((existingMeta.runtimeVars as RuntimeVarsMap) ?? {}),
...editRuntimeVars,
},
}
: item.metadata
await story.updateItem(item.id, {
name,
description: editDescription.trim() || null,
quantity,
equipped: item.location === 'inventory' ? editEquipped : false,
metadata: updatedMetadata,
})
cancelEdit()
@ -240,6 +297,20 @@
/>
</div>
<!-- Runtime Variables (Edit - Equipped) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={editRuntimeVars}
entityId={item.id}
editMode={true}
onValueChange={(defId, value) => {
const def = runtimeVarDefs.find((d) => d.id === defId)
if (def) updateEditRuntimeVar(defId, def.variableName, value)
}}
/>
{/if}
<div class="border-border flex justify-end gap-2 border-t pt-2">
<Button variant="text" size="sm" class="h-7 text-xs" onclick={cancelEdit}>
Cancel
@ -339,6 +410,15 @@
</div>
{/if}
<!-- Runtime Variables (Display - Equipped) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={item.metadata?.runtimeVars as RuntimeVarsMap | undefined}
entityId={item.id}
/>
{/if}
<!-- Footer Actions -->
<div class="mt-2 flex items-center justify-between">
<div class="-ml-1.5 flex items-center">
@ -452,6 +532,20 @@
/>
</div>
<!-- Runtime Variables (Edit - Backpack) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={editRuntimeVars}
entityId={item.id}
editMode={true}
onValueChange={(defId, value) => {
const def = runtimeVarDefs.find((d) => d.id === defId)
if (def) updateEditRuntimeVar(defId, def.variableName, value)
}}
/>
{/if}
<div class="border-border flex justify-end gap-2 border-t pt-2">
<Button variant="text" size="sm" class="h-7 text-xs" onclick={cancelEdit}>
Cancel
@ -543,6 +637,15 @@
</div>
{/if}
<!-- Runtime Variables (Display - Backpack) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={item.metadata?.runtimeVars as RuntimeVarsMap | undefined}
entityId={item.id}
/>
{/if}
<!-- Footer Actions -->
<div class="mt-2 flex items-center justify-between">
<div class="-ml-1.5 flex items-center">
@ -667,6 +770,20 @@
/>
</div>
<!-- Runtime Variables (Edit - World) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={editRuntimeVars}
entityId={item.id}
editMode={true}
onValueChange={(defId, value) => {
const def = runtimeVarDefs.find((d) => d.id === defId)
if (def) updateEditRuntimeVar(defId, def.variableName, value)
}}
/>
{/if}
<div class="border-border flex justify-end gap-2 border-t pt-2">
<Button variant="text" size="sm" class="h-7 text-xs" onclick={cancelEdit}>
Cancel
@ -766,6 +883,15 @@
</div>
{/if}
<!-- Runtime Variables (Display - World) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={item.metadata?.runtimeVars as RuntimeVarsMap | undefined}
entityId={item.id}
/>
{/if}
<!-- Footer Actions -->
<div class="mt-2 flex items-center justify-between">
<div class="-ml-1.5 flex items-center">

View file

@ -13,6 +13,7 @@
X,
} from 'lucide-svelte'
import type { StoryBeat } from '$lib/types'
import type { RuntimeVariable, RuntimeVarsMap } from '$lib/services/packs/types'
import { Button } from '$lib/components/ui/button'
import { Input } from '$lib/components/ui/input'
import { Textarea } from '$lib/components/ui/textarea'
@ -21,6 +22,8 @@
import * as Select from '$lib/components/ui/select'
import IconRow from '$lib/components/ui/icon-row.svelte'
import { cn } from '$lib/utils/cn'
import { database } from '$lib/services/database'
import RuntimeVariableDisplay from './RuntimeVariableDisplay.svelte'
let showAddForm = $state(false)
let newTitle = $state('')
@ -32,6 +35,41 @@
let editType = $state<StoryBeat['type']>('quest')
let editStatus = $state<StoryBeat['status']>('pending')
// Runtime variables
let runtimeVarDefs = $state<RuntimeVariable[]>([])
let editRuntimeVars = $state<RuntimeVarsMap>({})
$effect(() => {
if (story.currentStory) {
loadRuntimeVarDefs()
}
})
async function loadRuntimeVarDefs() {
if (!story.currentStory) return
try {
const packId = await database.getStoryPackId(story.currentStory.id)
if (packId) {
runtimeVarDefs = await database.getRuntimeVariablesByEntityType(packId, 'story_beat')
} else {
runtimeVarDefs = []
}
} catch {
runtimeVarDefs = []
}
}
function updateEditRuntimeVar(
defId: string,
variableName: string,
value: string | number | null,
) {
editRuntimeVars = {
...editRuntimeVars,
[defId]: { variableName, v: value },
}
}
function toggleCollapse(beatId: string) {
const isCollapsed = ui.isEntityCollapsed(beatId)
ui.toggleEntityCollapsed(beatId, !isCollapsed)
@ -52,6 +90,9 @@
editDescription = beat.description ?? ''
editType = beat.type
editStatus = beat.status
// Initialize runtime vars from entity metadata
const rv = (beat.metadata as Record<string, unknown> | null)?.runtimeVars
editRuntimeVars = rv && typeof rv === 'object' ? { ...(rv as RuntimeVarsMap) } : {}
}
function cancelEdit() {
@ -60,16 +101,32 @@
editDescription = ''
editType = 'quest'
editStatus = 'pending'
editRuntimeVars = {}
}
async function saveEdit(beat: StoryBeat) {
const title = editTitle.trim()
if (!title) return
// Merge runtime vars into metadata
const existingMeta = (beat.metadata as Record<string, unknown>) ?? {}
const hasRuntimeVarEdits = Object.keys(editRuntimeVars).length > 0
const updatedMetadata = hasRuntimeVarEdits
? {
...existingMeta,
runtimeVars: {
...((existingMeta.runtimeVars as RuntimeVarsMap) ?? {}),
...editRuntimeVars,
},
}
: beat.metadata
await story.updateStoryBeat(beat.id, {
title,
description: editDescription.trim() || null,
type: editType,
status: editStatus,
metadata: updatedMetadata,
})
cancelEdit()
}
@ -267,6 +324,20 @@
class="min-h-[60px] resize-none text-xs"
/>
</div>
<!-- Runtime Variables (Edit - Active) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={editRuntimeVars}
entityId={beat.id}
editMode={true}
onValueChange={(defId, value) => {
const def = runtimeVarDefs.find((d) => d.id === defId)
if (def) updateEditRuntimeVar(defId, def.variableName, value)
}}
/>
{/if}
</div>
<div class="border-border flex justify-end gap-2 border-t pt-2">
@ -318,6 +389,15 @@
</div>
{/if}
<!-- Runtime Variables (Display - Active) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={beat.metadata?.runtimeVars as RuntimeVarsMap | undefined}
entityId={beat.id}
/>
{/if}
<!-- Footer Actions -->
<div class="mt-2 flex items-center justify-between">
<div class="-ml-1.5 flex items-center">
@ -468,6 +548,20 @@
class="min-h-[60px] resize-none text-xs"
/>
</div>
<!-- Runtime Variables (Edit - History) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={editRuntimeVars}
entityId={beat.id}
editMode={true}
onValueChange={(defId, value) => {
const def = runtimeVarDefs.find((d) => d.id === defId)
if (def) updateEditRuntimeVar(defId, def.variableName, value)
}}
/>
{/if}
</div>
<div class="border-border flex justify-end gap-2 border-t pt-2">
@ -525,6 +619,15 @@
</div>
{/if}
<!-- Runtime Variables (Display - History) -->
{#if runtimeVarDefs.length > 0}
<RuntimeVariableDisplay
definitions={runtimeVarDefs}
values={beat.metadata?.runtimeVars as RuntimeVarsMap | undefined}
entityId={beat.id}
/>
{/if}
<!-- Footer Actions -->
<div class="mt-2 flex items-center justify-between">
<div class="-ml-1.5 flex items-center">