fix: character slot selection glow and empty protagonist
- character grid starts at position 1 (skip protagonist) - add isEmptySelected state for glow on empty slots - empty protagonist shows relief.png background only
This commit is contained in:
parent
18328e2d38
commit
d51fe03905
2 changed files with 56 additions and 9 deletions
|
|
@ -11,6 +11,7 @@
|
|||
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
|
||||
import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils'
|
||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import { GridType } from '$lib/types/enums'
|
||||
import perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg'
|
||||
import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg'
|
||||
import * as m from '$lib/paraglide/messages'
|
||||
|
|
@ -30,6 +31,8 @@
|
|||
updateParty: (p: Party) => void
|
||||
canEdit: () => boolean
|
||||
getEditKey: () => string | null
|
||||
getSelectedSlot?: () => number
|
||||
getActiveTab?: () => GridType
|
||||
services: { gridService: any; partyService: any }
|
||||
openPicker?: (opts: {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
|
|
@ -53,6 +56,11 @@
|
|||
return getJobPortraitUrl(job, Gender.Gran) // TODO: Get gender from user preferences
|
||||
}
|
||||
|
||||
// For protagonist slot without a job, show nothing (relief.png background shows through)
|
||||
if (position === 0 && !job) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// If no item or no character with granblueId, return placeholder
|
||||
if (!item || !item.character?.granblueId) {
|
||||
return getCharacterImageWithPose(null, 'main', 0, 0)
|
||||
|
|
@ -74,6 +82,14 @@
|
|||
// Check if this item is currently active in the sidebar
|
||||
let isActive = $derived(item?.id && sidebar.activeItemId === String(item.id))
|
||||
|
||||
// Check if this empty slot is currently selected for adding an item
|
||||
let isEmptySelected = $derived(
|
||||
!item &&
|
||||
!isProtagonist &&
|
||||
ctx?.getSelectedSlot?.() === position &&
|
||||
ctx?.getActiveTab?.() === GridType.Character
|
||||
)
|
||||
|
||||
// Determine element class for focus ring
|
||||
let elementClass = $derived.by(() => {
|
||||
const element = item?.character?.element || partyElement
|
||||
|
|
@ -180,6 +196,7 @@
|
|||
class="frame character cell {elementClass}"
|
||||
class:protagonist={position === 0}
|
||||
class:editable={ctx?.canEdit()}
|
||||
class:is-active={isActive}
|
||||
onclick={() => viewDetails()}
|
||||
>
|
||||
{#if position !== 0}
|
||||
|
|
@ -210,6 +227,7 @@
|
|||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if imageUrl}
|
||||
<img
|
||||
class="image {elementClass}"
|
||||
class:placeholder={!item?.character?.granblueId && !isProtagonist}
|
||||
|
|
@ -217,6 +235,7 @@
|
|||
alt={isProtagonist && job ? job.name.en : displayName(item?.character)}
|
||||
src={imageUrl}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
|
@ -254,20 +273,28 @@
|
|||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit() && !isProtagonist}
|
||||
class:protagonist={isProtagonist}
|
||||
class:empty-protagonist={isProtagonist && !job}
|
||||
class:is-selected={isEmptySelected}
|
||||
onclick={() =>
|
||||
!isProtagonist &&
|
||||
ctx?.canEdit() &&
|
||||
ctx?.openPicker &&
|
||||
ctx.openPicker({ type: 'character', position, item })}
|
||||
>
|
||||
<img
|
||||
class="image"
|
||||
class:placeholder={!isProtagonist || !job}
|
||||
class:protagonist={isProtagonist}
|
||||
alt={isProtagonist && job ? job.name.en : ''}
|
||||
src={isProtagonist ? imageUrl : '/images/placeholders/placeholder-weapon-grid.png'}
|
||||
/>
|
||||
{#if ctx?.canEdit()}
|
||||
{#if !isProtagonist}
|
||||
<img
|
||||
class="image placeholder"
|
||||
alt=""
|
||||
src="/images/placeholders/placeholder-weapon-grid.png"
|
||||
/>
|
||||
{:else if job && imageUrl}
|
||||
<img
|
||||
class="image protagonist"
|
||||
alt={job.name.en}
|
||||
src={imageUrl}
|
||||
/>
|
||||
{/if}
|
||||
{#if ctx?.canEdit() && !isProtagonist}
|
||||
<span class="icon">
|
||||
<Icon name="plus" size={24} />
|
||||
</span>
|
||||
|
|
@ -381,6 +408,22 @@
|
|||
opacity: 0.95;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// Slot selection - subtle dark pulsing glow (works for both empty and filled)
|
||||
&.is-selected,
|
||||
&.is-active {
|
||||
animation: pulse-slot-shadow 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-slot-shadow {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12), 0 0 4px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.24), 0 0 8px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.frame.character.cell {
|
||||
|
|
@ -391,6 +434,10 @@
|
|||
background-size: cover;
|
||||
background-position: center -20px;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&.empty-protagonist {
|
||||
background-position: center 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface SlotRange {
|
|||
const GRID_CONFIGS: Record<GridType, SlotRange> = {
|
||||
[GridType.Weapon]: { start: 0, end: 8, specialSlots: [-1] }, // mainhand + 9 grid slots
|
||||
[GridType.Summon]: { start: 0, end: 5, specialSlots: [-1, 6] }, // main + 6 grid + friend
|
||||
[GridType.Character]: { start: 0, end: 4, specialSlots: [] } // 5 slots (0-4)
|
||||
[GridType.Character]: { start: 1, end: 4, specialSlots: [] } // 4 slots (1-4), position 0 is protagonist (not user-selectable)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue