add extra_prerequisite and forge chain to weapon database pages
This commit is contained in:
parent
33b578ec21
commit
a01c6e8d31
8 changed files with 604 additions and 7 deletions
364
src/lib/components/ui/WeaponTypeahead.svelte
Normal file
364
src/lib/components/ui/WeaponTypeahead.svelte
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
<!-- WeaponTypeahead Component - Async weapon search with Svelecte -->
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Svelecte from 'svelecte'
|
||||||
|
import Icon from '../Icon.svelte'
|
||||||
|
import { searchAdapter, type SearchResult } from '$lib/api/adapters/search.adapter'
|
||||||
|
import { getWeaponGridImage } from '$lib/utils/images'
|
||||||
|
|
||||||
|
interface WeaponOption {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
granblueId: string
|
||||||
|
element?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Selected weapon granblue ID (e.g. "1040001000") */
|
||||||
|
value?: string | null
|
||||||
|
/** Initial weapon data for display (when loading existing value) */
|
||||||
|
initialWeapon?: { id: string; name: string; granblueId: string } | null
|
||||||
|
/** Callback when value changes */
|
||||||
|
onValueChange?: (granblueId: string | null) => void
|
||||||
|
/** Placeholder text */
|
||||||
|
placeholder?: string
|
||||||
|
/** Disabled state */
|
||||||
|
disabled?: boolean
|
||||||
|
/** Component size */
|
||||||
|
size?: 'small' | 'medium' | 'large'
|
||||||
|
/** Clear button visible */
|
||||||
|
clearable?: boolean
|
||||||
|
/** Minimum characters before search */
|
||||||
|
minQuery?: number
|
||||||
|
/** Use contained styling (for use inside containers) */
|
||||||
|
contained?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
value = $bindable(null),
|
||||||
|
initialWeapon = null,
|
||||||
|
onValueChange,
|
||||||
|
placeholder = 'Search weapons...',
|
||||||
|
disabled = false,
|
||||||
|
size = 'medium',
|
||||||
|
clearable = true,
|
||||||
|
minQuery = 2,
|
||||||
|
contained = false
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
let searchResults = $state<WeaponOption[]>([])
|
||||||
|
// Only used when user selects something NEW (different from initialWeapon)
|
||||||
|
let userSelectedOption = $state<WeaponOption | null>(null)
|
||||||
|
let isLoading = $state(false)
|
||||||
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
// Clear userSelectedOption when value is cleared
|
||||||
|
$effect(() => {
|
||||||
|
if (!value) {
|
||||||
|
userSelectedOption = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Derive options: include initialWeapon or userSelectedOption so Svelecte can find the value
|
||||||
|
const options = $derived.by(() => {
|
||||||
|
const results = [...searchResults]
|
||||||
|
|
||||||
|
// If user selected something new, prioritize that
|
||||||
|
const userSelected = userSelectedOption
|
||||||
|
if (userSelected && !results.find((o) => o.granblueId === userSelected.granblueId)) {
|
||||||
|
return [userSelected, ...results]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, include initialWeapon if we have a value matching it
|
||||||
|
if (value && initialWeapon && initialWeapon.granblueId === value) {
|
||||||
|
const initOption: WeaponOption = {
|
||||||
|
id: initialWeapon.id,
|
||||||
|
label: initialWeapon.name,
|
||||||
|
granblueId: initialWeapon.granblueId
|
||||||
|
}
|
||||||
|
if (!results.find((o) => o.granblueId === initOption.granblueId)) {
|
||||||
|
return [initOption, ...results]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
})
|
||||||
|
|
||||||
|
const typeaheadClasses = $derived(
|
||||||
|
['weapon-typeahead', size, contained && 'contained', disabled && 'disabled']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
)
|
||||||
|
|
||||||
|
async function searchWeapons(query: string) {
|
||||||
|
if (query.length < minQuery) {
|
||||||
|
searchResults = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
const response = await searchAdapter.searchWeapons({
|
||||||
|
query,
|
||||||
|
per: 20,
|
||||||
|
locale: 'en'
|
||||||
|
})
|
||||||
|
|
||||||
|
searchResults = response.results.map((result: SearchResult) => ({
|
||||||
|
id: result.id,
|
||||||
|
label: result.name?.en || result.name?.ja || result.granblueId,
|
||||||
|
granblueId: result.granblueId,
|
||||||
|
element: result.element
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Weapon search error:', error)
|
||||||
|
searchResults = []
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement | null
|
||||||
|
const query = target?.value ?? ''
|
||||||
|
|
||||||
|
// Debounce the search
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
searchWeapons(query)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(selected: WeaponOption | null) {
|
||||||
|
const newValue = selected?.granblueId || null
|
||||||
|
value = newValue
|
||||||
|
// Only track as userSelectedOption if it's different from initialWeapon
|
||||||
|
if (selected && initialWeapon && selected.granblueId === initialWeapon.granblueId) {
|
||||||
|
userSelectedOption = null // Use initialWeapon instead
|
||||||
|
} else {
|
||||||
|
userSelectedOption = selected
|
||||||
|
}
|
||||||
|
onValueChange?.(newValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class={typeaheadClasses} oninput={handleInput}>
|
||||||
|
<Svelecte
|
||||||
|
{options}
|
||||||
|
value={value}
|
||||||
|
labelField="label"
|
||||||
|
valueField="granblueId"
|
||||||
|
searchable={true}
|
||||||
|
{placeholder}
|
||||||
|
{disabled}
|
||||||
|
{clearable}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{#snippet toggleIcon(dropdownShow)}
|
||||||
|
<Icon name="chevron-down-small" size={14} class="chevron" />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet option(opt)}
|
||||||
|
{@const weapon = opt as WeaponOption}
|
||||||
|
<div class="option-item">
|
||||||
|
<img
|
||||||
|
src={getWeaponGridImage(weapon.granblueId, weapon.element)}
|
||||||
|
alt=""
|
||||||
|
class="option-image"
|
||||||
|
/>
|
||||||
|
<span class="option-label">{weapon.label}</span>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet selection(sel)}
|
||||||
|
{@const weapon = (sel as WeaponOption[])[0]}
|
||||||
|
{#if weapon}
|
||||||
|
<div class="selection-item">
|
||||||
|
<img
|
||||||
|
src={getWeaponGridImage(weapon.granblueId, weapon.element)}
|
||||||
|
alt=""
|
||||||
|
class="selection-image"
|
||||||
|
/>
|
||||||
|
<span class="selection-label">{weapon.label}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</Svelecte>
|
||||||
|
{#if isLoading}
|
||||||
|
<span class="loading-indicator">...</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/colors' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/mixins' as *;
|
||||||
|
@use '$src/themes/effects' as *;
|
||||||
|
|
||||||
|
.weapon-typeahead {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
// Svelecte CSS variable overrides
|
||||||
|
--sv-bg: var(--input-bg);
|
||||||
|
--sv-border-color: transparent;
|
||||||
|
--sv-border: 1px solid var(--sv-border-color);
|
||||||
|
--sv-active-border: 1px solid #{$blue};
|
||||||
|
--sv-active-outline: none;
|
||||||
|
--sv-border-radius: #{$input-corner};
|
||||||
|
--sv-min-height: #{$unit-4x};
|
||||||
|
--sv-placeholder-color: var(--text-tertiary);
|
||||||
|
--sv-color: var(--text-primary);
|
||||||
|
|
||||||
|
--sv-dropdown-bg: var(--dialog-bg);
|
||||||
|
--sv-dropdown-border-radius: #{$card-corner};
|
||||||
|
--sv-dropdown-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
--sv-dropdown-offset: #{$unit-half};
|
||||||
|
|
||||||
|
--sv-item-color: var(--text-primary);
|
||||||
|
--sv-item-active-bg: var(--option-bg-hover);
|
||||||
|
--sv-item-selected-bg: var(--option-bg-hover);
|
||||||
|
|
||||||
|
--sv-icon-color: var(--text-tertiary);
|
||||||
|
--sv-icon-hover-color: var(--text-primary);
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target Svelecte control for hover states
|
||||||
|
:global(.sv-control) {
|
||||||
|
padding: calc($unit-half + 1px) $unit calc($unit-half + 1px) $unit-half;
|
||||||
|
@include smooth-transition($duration-quick, background-color, border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.disabled) :global(.sv-control) {
|
||||||
|
background-color: var(--input-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contained variant
|
||||||
|
&.contained {
|
||||||
|
--sv-bg: var(--select-contained-bg);
|
||||||
|
|
||||||
|
&:hover:not(.disabled) :global(.sv-control) {
|
||||||
|
background-color: var(--select-contained-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style the dropdown
|
||||||
|
:global(.sv_dropdown) {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: 40vh;
|
||||||
|
z-index: 102;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style dropdown items
|
||||||
|
:global(.sv-item) {
|
||||||
|
border-radius: $item-corner-small;
|
||||||
|
padding: $unit $unit-2x;
|
||||||
|
gap: $unit;
|
||||||
|
@include smooth-transition($duration-quick, background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style the input text
|
||||||
|
:global(.sv-input--text) {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style the indicator buttons
|
||||||
|
:global(.sv-btn-indicator) {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
@include smooth-transition($duration-quick, color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style our custom chevron icon
|
||||||
|
:global(.chevron) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the separator bar between buttons
|
||||||
|
:global(.sv-btn-separator) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom option item styling
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-image {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: $item-corner-small;
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom selection item styling (shown in input when value selected)
|
||||||
|
.selection-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-half;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-image {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: $item-corner-small;
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size variants
|
||||||
|
.weapon-typeahead.small {
|
||||||
|
--sv-min-height: #{$unit-3x};
|
||||||
|
--sv-font-size: #{$font-small};
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon-typeahead.medium {
|
||||||
|
--sv-min-height: #{$unit-4x};
|
||||||
|
--sv-font-size: #{$font-regular};
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon-typeahead.large {
|
||||||
|
--sv-min-height: calc(#{$unit} * 6);
|
||||||
|
--sv-font-size: #{$font-large};
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: $unit-3x;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: $font-small;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
|
import WeaponTypeahead from '$lib/components/ui/WeaponTypeahead.svelte'
|
||||||
|
import { getWeaponGridImage } from '$lib/utils/images'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
weapon: any
|
||||||
|
editMode?: boolean
|
||||||
|
editData?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
let { weapon, editMode = false, editData = $bindable() }: Props = $props()
|
||||||
|
|
||||||
|
// Get forge chain for display (only in view mode)
|
||||||
|
const forgeChain = $derived(weapon?.forgeChain ?? [])
|
||||||
|
const forgedFrom = $derived(weapon?.forgedFrom ?? null)
|
||||||
|
const forgeOrder = $derived(editMode ? editData?.forgeOrder : weapon?.forgeOrder)
|
||||||
|
|
||||||
|
// Check if weapon has any forge data
|
||||||
|
const hasForgeData = $derived(
|
||||||
|
forgeChain.length > 0 || forgedFrom != null || forgeOrder != null || editMode
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get initial weapon data for typeahead
|
||||||
|
const initialForgedFrom = $derived.by(() => {
|
||||||
|
if (!forgedFrom) return null
|
||||||
|
return {
|
||||||
|
id: forgedFrom.id,
|
||||||
|
name: forgedFrom.name?.en || forgedFrom.name?.ja || forgedFrom.granblueId,
|
||||||
|
granblueId: forgedFrom.granblueId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if hasForgeData}
|
||||||
|
<DetailsContainer title="Forge Chain">
|
||||||
|
{#if editMode}
|
||||||
|
<DetailItem label="Forged From" sublabel="The weapon this was forged from">
|
||||||
|
<WeaponTypeahead
|
||||||
|
bind:value={editData.forgedFrom}
|
||||||
|
initialWeapon={initialForgedFrom}
|
||||||
|
placeholder="Search for base weapon..."
|
||||||
|
contained
|
||||||
|
/>
|
||||||
|
</DetailItem>
|
||||||
|
<DetailItem
|
||||||
|
label="Forge Order"
|
||||||
|
sublabel="Position in chain (0=base, 1=first forge, etc.)"
|
||||||
|
bind:value={editData.forgeOrder}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
{#if forgeChain.length > 0}
|
||||||
|
<DetailItem label="Forge Chain">
|
||||||
|
<div class="forge-chain">
|
||||||
|
{#each forgeChain as chainWeapon, index}
|
||||||
|
<a
|
||||||
|
href="/database/weapons/{chainWeapon.granblueId}"
|
||||||
|
class="chain-item"
|
||||||
|
class:current={chainWeapon.granblueId === weapon.granblueId}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getWeaponGridImage(chainWeapon.granblueId, weapon.element)}
|
||||||
|
alt=""
|
||||||
|
class="chain-image"
|
||||||
|
/>
|
||||||
|
<span class="chain-name">{chainWeapon.name?.en || chainWeapon.name?.ja}</span>
|
||||||
|
<span class="chain-order">({chainWeapon.forgeOrder})</span>
|
||||||
|
</a>
|
||||||
|
{#if index < forgeChain.length - 1}
|
||||||
|
<span class="chain-arrow">→</span>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</DetailItem>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if forgedFrom && forgeChain.length === 0}
|
||||||
|
<DetailItem label="Forged From">
|
||||||
|
<a href="/database/weapons/{forgedFrom.granblueId}" class="forged-from-link">
|
||||||
|
{forgedFrom.name?.en || forgedFrom.name?.ja}
|
||||||
|
</a>
|
||||||
|
</DetailItem>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if forgeOrder != null}
|
||||||
|
<DetailItem label="Forge Order" value={forgeOrder.toString()} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</DetailsContainer>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
@use '$src/themes/layout' as layout;
|
||||||
|
|
||||||
|
.forge-chain {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
padding: spacing.$unit-half spacing.$unit;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: layout.$item-corner-small;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--card-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
background: colors.$blue--bg--light;
|
||||||
|
outline: 1px solid colors.$blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-image {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-name {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-order {
|
||||||
|
font-size: typography.$font-xs;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-arrow {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forged-from-link {
|
||||||
|
color: colors.$blue;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -46,6 +46,22 @@
|
||||||
const uncapLevel = $derived(transcendence ? 6 : ulb ? 5 : flb ? 4 : 3)
|
const uncapLevel = $derived(transcendence ? 6 : ulb ? 5 : flb ? 4 : 3)
|
||||||
const transcendenceStage = $derived(transcendence ? 5 : 0)
|
const transcendenceStage = $derived(transcendence ? 5 : 0)
|
||||||
|
|
||||||
|
// Extra prerequisite options for dropdown
|
||||||
|
const extraPrerequisiteOptions = [
|
||||||
|
{ value: '', label: 'None' },
|
||||||
|
{ value: 3, label: 'MLB' },
|
||||||
|
{ value: 4, label: 'FLB' },
|
||||||
|
{ value: 5, label: 'ULB' },
|
||||||
|
{ value: 6, label: 'Transcendence' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Get label for extra prerequisite value
|
||||||
|
function getExtraPrerequisiteLabel(value: number | null | undefined): string {
|
||||||
|
if (value == null) return '—'
|
||||||
|
const option = extraPrerequisiteOptions.find((o) => o.value === value)
|
||||||
|
return option?.label ?? '—'
|
||||||
|
}
|
||||||
|
|
||||||
// Get element name for checkbox theming
|
// Get element name for checkbox theming
|
||||||
const elementName = $derived.by((): ElementName | undefined => {
|
const elementName = $derived.by((): ElementName | undefined => {
|
||||||
const el = editMode ? editData.element : weapon?.element
|
const el = editMode ? editData.element : weapon?.element
|
||||||
|
|
@ -132,5 +148,15 @@
|
||||||
element={elementName}
|
element={elementName}
|
||||||
onchange={handleTranscendenceChange}
|
onchange={handleTranscendenceChange}
|
||||||
/>
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Extra Prerequisite"
|
||||||
|
sublabel="Min uncap for Additional Weapons"
|
||||||
|
bind:value={editData.extraPrerequisite}
|
||||||
|
editable={true}
|
||||||
|
type="select"
|
||||||
|
options={extraPrerequisiteOptions}
|
||||||
|
/>
|
||||||
|
{:else if weapon?.uncap?.extraPrerequisite != null}
|
||||||
|
<DetailItem label="Extra Prerequisite" value={getExtraPrerequisiteLabel(weapon.uncap.extraPrerequisite)} />
|
||||||
{/if}
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export interface Weapon {
|
||||||
flb: boolean
|
flb: boolean
|
||||||
ulb: boolean
|
ulb: boolean
|
||||||
transcendence: boolean
|
transcendence: boolean
|
||||||
|
extraPrerequisite?: number | null
|
||||||
}
|
}
|
||||||
transcendenceHp?: number
|
transcendenceHp?: number
|
||||||
transcendenceAtk?: number
|
transcendenceAtk?: number
|
||||||
|
|
@ -57,6 +58,10 @@ export interface Weapon {
|
||||||
kamigame?: string
|
kamigame?: string
|
||||||
nicknames?: { en?: string[]; ja?: string[] }
|
nicknames?: { en?: string[]; ja?: string[] }
|
||||||
recruits?: string | { id: string; granblueId: string; name: LocalizedName }
|
recruits?: string | { id: string; granblueId: string; name: LocalizedName }
|
||||||
|
// Forge chain fields
|
||||||
|
forgeOrder?: number | null
|
||||||
|
forgedFrom?: { id: string; granblueId: string; name: LocalizedName } | null
|
||||||
|
forgeChain?: Array<{ id: string; granblueId: string; name: LocalizedName; forgeOrder: number }> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character entity from CharacterBlueprint
|
// Character entity from CharacterBlueprint
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
||||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||||
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
||||||
|
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.svelte'
|
||||||
import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
|
import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
|
||||||
import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
|
import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
|
||||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
|
|
@ -205,6 +206,7 @@
|
||||||
<WeaponUncapSection {weapon} />
|
<WeaponUncapSection {weapon} />
|
||||||
<WeaponTaxonomySection {weapon} />
|
<WeaponTaxonomySection {weapon} />
|
||||||
<WeaponStatsSection {weapon} />
|
<WeaponStatsSection {weapon} />
|
||||||
|
<WeaponForgeSection {weapon} />
|
||||||
|
|
||||||
<DetailsContainer title="Nicknames">
|
<DetailsContainer title="Nicknames">
|
||||||
<DetailItem label="English">
|
<DetailItem label="English">
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
||||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||||
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
||||||
|
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.svelte'
|
||||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
import TagInput from '$lib/components/ui/TagInput.svelte'
|
import TagInput from '$lib/components/ui/TagInput.svelte'
|
||||||
|
|
@ -74,6 +75,7 @@
|
||||||
flb: false,
|
flb: false,
|
||||||
ulb: false,
|
ulb: false,
|
||||||
transcendence: false,
|
transcendence: false,
|
||||||
|
extraPrerequisite: '' as number | '',
|
||||||
extra: false,
|
extra: false,
|
||||||
limit: false,
|
limit: false,
|
||||||
ax: false,
|
ax: false,
|
||||||
|
|
@ -88,7 +90,10 @@
|
||||||
kamigame: '',
|
kamigame: '',
|
||||||
nicknamesEn: [] as string[],
|
nicknamesEn: [] as string[],
|
||||||
nicknamesJp: [] as string[],
|
nicknamesJp: [] as string[],
|
||||||
recruits: ''
|
recruits: '',
|
||||||
|
// Forge chain fields
|
||||||
|
forgedFrom: '' as string | null,
|
||||||
|
forgeOrder: null as number | null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Populate edit data when weapon loads
|
// Populate edit data when weapon loads
|
||||||
|
|
@ -117,6 +122,7 @@
|
||||||
flb: weapon.uncap?.flb || false,
|
flb: weapon.uncap?.flb || false,
|
||||||
ulb: weapon.uncap?.ulb || false,
|
ulb: weapon.uncap?.ulb || false,
|
||||||
transcendence: weapon.uncap?.transcendence || false,
|
transcendence: weapon.uncap?.transcendence || false,
|
||||||
|
extraPrerequisite: weapon.uncap?.extraPrerequisite ?? '',
|
||||||
extra: weapon.extra || false,
|
extra: weapon.extra || false,
|
||||||
limit: Boolean(weapon.limit),
|
limit: Boolean(weapon.limit),
|
||||||
ax: weapon.ax || false,
|
ax: weapon.ax || false,
|
||||||
|
|
@ -131,7 +137,10 @@
|
||||||
kamigame: weapon.kamigame || '',
|
kamigame: weapon.kamigame || '',
|
||||||
nicknamesEn: weapon.nicknames?.en || [],
|
nicknamesEn: weapon.nicknames?.en || [],
|
||||||
nicknamesJp: weapon.nicknames?.ja || [],
|
nicknamesJp: weapon.nicknames?.ja || [],
|
||||||
recruits: typeof weapon.recruits === 'string' ? weapon.recruits : (weapon.recruits?.granblueId ?? '')
|
recruits: typeof weapon.recruits === 'string' ? weapon.recruits : (weapon.recruits?.granblueId ?? ''),
|
||||||
|
// Forge chain fields
|
||||||
|
forgedFrom: weapon.forgedFrom?.granblueId || null,
|
||||||
|
forgeOrder: weapon.forgeOrder ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -165,6 +174,7 @@
|
||||||
flb: editData.flb,
|
flb: editData.flb,
|
||||||
ulb: editData.ulb,
|
ulb: editData.ulb,
|
||||||
transcendence: editData.transcendence,
|
transcendence: editData.transcendence,
|
||||||
|
extra_prerequisite: editData.extraPrerequisite === '' ? null : editData.extraPrerequisite,
|
||||||
extra: editData.extra,
|
extra: editData.extra,
|
||||||
limit: editData.limit,
|
limit: editData.limit,
|
||||||
ax: editData.ax,
|
ax: editData.ax,
|
||||||
|
|
@ -179,7 +189,10 @@
|
||||||
kamigame: editData.kamigame,
|
kamigame: editData.kamigame,
|
||||||
nicknames_en: editData.nicknamesEn,
|
nicknames_en: editData.nicknamesEn,
|
||||||
nicknames_jp: editData.nicknamesJp,
|
nicknames_jp: editData.nicknamesJp,
|
||||||
recruits: editData.recruits || undefined
|
recruits: editData.recruits || undefined,
|
||||||
|
// Forge chain fields
|
||||||
|
forged_from: editData.forgedFrom || null,
|
||||||
|
forge_order: editData.forgeOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
await entityAdapter.updateWeapon(weapon.id, payload)
|
await entityAdapter.updateWeapon(weapon.id, payload)
|
||||||
|
|
@ -226,6 +239,7 @@
|
||||||
<WeaponUncapSection {weapon} {editMode} bind:editData />
|
<WeaponUncapSection {weapon} {editMode} bind:editData />
|
||||||
<WeaponTaxonomySection {weapon} {editMode} bind:editData />
|
<WeaponTaxonomySection {weapon} {editMode} bind:editData />
|
||||||
<WeaponStatsSection {weapon} {editMode} bind:editData />
|
<WeaponStatsSection {weapon} {editMode} bind:editData />
|
||||||
|
<WeaponForgeSection {weapon} {editMode} bind:editData />
|
||||||
|
|
||||||
<DetailsContainer title="Nicknames">
|
<DetailsContainer title="Nicknames">
|
||||||
<DetailItem label="Nicknames (EN)">
|
<DetailItem label="Nicknames (EN)">
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||||
import WeaponMetadataSection from '$lib/features/database/weapons/sections/WeaponMetadataSection.svelte'
|
import WeaponMetadataSection from '$lib/features/database/weapons/sections/WeaponMetadataSection.svelte'
|
||||||
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
||||||
|
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.svelte'
|
||||||
import TabbedEntitySelector from '$lib/features/database/import/TabbedEntitySelector.svelte'
|
import TabbedEntitySelector from '$lib/features/database/import/TabbedEntitySelector.svelte'
|
||||||
import type { EntityTab } from '$lib/features/database/import/TabbedEntitySelector.svelte'
|
import type { EntityTab } from '$lib/features/database/import/TabbedEntitySelector.svelte'
|
||||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
|
|
@ -123,6 +124,7 @@
|
||||||
flb: suggestions?.flb ?? false,
|
flb: suggestions?.flb ?? false,
|
||||||
ulb: suggestions?.ulb ?? false,
|
ulb: suggestions?.ulb ?? false,
|
||||||
transcendence: suggestions?.transcendence ?? false,
|
transcendence: suggestions?.transcendence ?? false,
|
||||||
|
extraPrerequisite: '' as number | '',
|
||||||
extra: false,
|
extra: false,
|
||||||
limit: false,
|
limit: false,
|
||||||
ax: false,
|
ax: false,
|
||||||
|
|
@ -136,7 +138,10 @@
|
||||||
kamigame: suggestions?.kamigame ?? '',
|
kamigame: suggestions?.kamigame ?? '',
|
||||||
nicknamesEn: [] as string[],
|
nicknamesEn: [] as string[],
|
||||||
nicknamesJp: [] as string[],
|
nicknamesJp: [] as string[],
|
||||||
recruits: suggestions?.recruits ?? null
|
recruits: suggestions?.recruits ?? null,
|
||||||
|
// Forge chain
|
||||||
|
forgedFrom: null as string | null,
|
||||||
|
forgeOrder: null as number | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,6 +284,7 @@
|
||||||
flb: formData.flb,
|
flb: formData.flb,
|
||||||
ulb: formData.ulb,
|
ulb: formData.ulb,
|
||||||
transcendence: formData.transcendence,
|
transcendence: formData.transcendence,
|
||||||
|
extra_prerequisite: formData.extraPrerequisite === '' ? null : formData.extraPrerequisite,
|
||||||
extra: formData.extra,
|
extra: formData.extra,
|
||||||
limit: formData.limit,
|
limit: formData.limit,
|
||||||
ax: formData.ax,
|
ax: formData.ax,
|
||||||
|
|
@ -293,7 +299,10 @@
|
||||||
nicknames_en: formData.nicknamesEn,
|
nicknames_en: formData.nicknamesEn,
|
||||||
nicknames_jp: formData.nicknamesJp,
|
nicknames_jp: formData.nicknamesJp,
|
||||||
recruits: formData.recruits,
|
recruits: formData.recruits,
|
||||||
wiki_raw: wikiRawByPage[selectedWikiPage] || undefined
|
wiki_raw: wikiRawByPage[selectedWikiPage] || undefined,
|
||||||
|
// Forge chain
|
||||||
|
forged_from: formData.forgedFrom || null,
|
||||||
|
forge_order: formData.forgeOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWeapon = await entityAdapter.createWeapon(payload)
|
const newWeapon = await entityAdapter.createWeapon(payload)
|
||||||
|
|
@ -510,6 +519,12 @@
|
||||||
onDismissSuggestion={handleDismissSuggestion}
|
onDismissSuggestion={handleDismissSuggestion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WeaponForgeSection
|
||||||
|
weapon={emptyWeapon}
|
||||||
|
editMode={true}
|
||||||
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
|
/>
|
||||||
|
|
||||||
<DetailsContainer title="Nicknames">
|
<DetailsContainer title="Nicknames">
|
||||||
<DetailItem label="Nicknames (EN)">
|
<DetailItem label="Nicknames (EN)">
|
||||||
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesEn} placeholder="Add nickname..." contained />
|
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesEn} placeholder="Add nickname..." contained />
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte'
|
import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte'
|
||||||
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
||||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||||
|
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.svelte'
|
||||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
|
import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
|
||||||
|
|
@ -79,6 +80,7 @@
|
||||||
flb: false,
|
flb: false,
|
||||||
ulb: false,
|
ulb: false,
|
||||||
transcendence: false,
|
transcendence: false,
|
||||||
|
extraPrerequisite: '' as number | '',
|
||||||
extra: false,
|
extra: false,
|
||||||
limit: false,
|
limit: false,
|
||||||
ax: false,
|
ax: false,
|
||||||
|
|
@ -100,7 +102,11 @@
|
||||||
nicknamesJp: [] as string[],
|
nicknamesJp: [] as string[],
|
||||||
|
|
||||||
// Recruits (Character ID)
|
// Recruits (Character ID)
|
||||||
recruits: null as string | null
|
recruits: null as string | null,
|
||||||
|
|
||||||
|
// Forge chain
|
||||||
|
forgedFrom: null as string | null,
|
||||||
|
forgeOrder: null as number | null
|
||||||
})
|
})
|
||||||
|
|
||||||
const rarityOptions = getRarityOptions()
|
const rarityOptions = getRarityOptions()
|
||||||
|
|
@ -183,6 +189,7 @@
|
||||||
flb: editData.flb,
|
flb: editData.flb,
|
||||||
ulb: editData.ulb,
|
ulb: editData.ulb,
|
||||||
transcendence: editData.transcendence,
|
transcendence: editData.transcendence,
|
||||||
|
extra_prerequisite: editData.extraPrerequisite === '' ? null : editData.extraPrerequisite,
|
||||||
extra: editData.extra,
|
extra: editData.extra,
|
||||||
limit: editData.limit,
|
limit: editData.limit,
|
||||||
ax: editData.ax,
|
ax: editData.ax,
|
||||||
|
|
@ -204,7 +211,11 @@
|
||||||
nicknames_jp: editData.nicknamesJp,
|
nicknames_jp: editData.nicknamesJp,
|
||||||
|
|
||||||
// Recruits
|
// Recruits
|
||||||
recruits: editData.recruits
|
recruits: editData.recruits,
|
||||||
|
|
||||||
|
// Forge chain
|
||||||
|
forged_from: editData.forgedFrom || null,
|
||||||
|
forge_order: editData.forgeOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWeapon = await entityAdapter.createWeapon(payload)
|
const newWeapon = await entityAdapter.createWeapon(payload)
|
||||||
|
|
@ -285,6 +296,7 @@
|
||||||
<WeaponUncapSection weapon={emptyWeapon} {editMode} bind:editData />
|
<WeaponUncapSection weapon={emptyWeapon} {editMode} bind:editData />
|
||||||
<WeaponTaxonomySection weapon={emptyWeapon} {editMode} bind:editData />
|
<WeaponTaxonomySection weapon={emptyWeapon} {editMode} bind:editData />
|
||||||
<WeaponStatsSection weapon={emptyWeapon} {editMode} bind:editData />
|
<WeaponStatsSection weapon={emptyWeapon} {editMode} bind:editData />
|
||||||
|
<WeaponForgeSection weapon={emptyWeapon} {editMode} bind:editData />
|
||||||
|
|
||||||
<DetailsContainer title="Nicknames">
|
<DetailsContainer title="Nicknames">
|
||||||
<DetailItem label="Nicknames (EN)">
|
<DetailItem label="Nicknames (EN)">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue