update grid and unit components
This commit is contained in:
parent
a00b8a8d18
commit
6b40a3dec6
4 changed files with 107 additions and 16 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { GridCharacter } from '$lib/types/api/party'
|
||||
import type { Job } from '$lib/types/api/entities'
|
||||
import { getContext } from 'svelte'
|
||||
import type { PartyContext } from '$lib/services/party.service'
|
||||
import type { DragDropContext } from '$lib/composables/drag-drop.svelte'
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
characters?: GridCharacter[]
|
||||
mainWeaponElement?: number | null | undefined
|
||||
partyElement?: number | null | undefined
|
||||
job?: Job
|
||||
container?: string
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +21,7 @@
|
|||
characters = [],
|
||||
mainWeaponElement = undefined,
|
||||
partyElement = undefined,
|
||||
job = undefined,
|
||||
container = 'main-characters'
|
||||
}: Props = $props()
|
||||
|
||||
|
|
@ -65,11 +68,23 @@
|
|||
type="character"
|
||||
canDrag={!!character && (ctx?.canEdit() ?? false)}
|
||||
>
|
||||
<CharacterUnit item={character} position={i} {mainWeaponElement} {partyElement} />
|
||||
<CharacterUnit
|
||||
item={character}
|
||||
position={i}
|
||||
{mainWeaponElement}
|
||||
{partyElement}
|
||||
job={i === 0 ? job : undefined}
|
||||
/>
|
||||
</DraggableItem>
|
||||
</DropZone>
|
||||
{:else}
|
||||
<CharacterUnit item={character} position={i} {mainWeaponElement} {partyElement} />
|
||||
<CharacterUnit
|
||||
item={character}
|
||||
position={i}
|
||||
{mainWeaponElement}
|
||||
{partyElement}
|
||||
job={i === 0 ? job : undefined}
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { Party, GridWeapon, GridCharacter } from '$lib/types/api/party'
|
||||
import type { Job } from '$lib/types/api/entities'
|
||||
import { getElementClass } from '$lib/types/enums'
|
||||
import { getCharacterImageWithPose } from '$lib/utils/images'
|
||||
import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils'
|
||||
|
||||
interface Props {
|
||||
party?: Party
|
||||
characters?: GridCharacter[]
|
||||
job?: Job
|
||||
jobId?: string
|
||||
element?: number
|
||||
gender?: number
|
||||
}
|
||||
|
||||
let { party, characters: directCharacters, jobId, element, gender }: Props = $props()
|
||||
let { party, characters: directCharacters, job, jobId, element, gender }: Props = $props()
|
||||
|
||||
// Use direct characters if provided, otherwise get from party
|
||||
const characters = $derived(directCharacters || party?.characters || [])
|
||||
|
|
@ -19,6 +22,10 @@
|
|||
characters.find((c: GridCharacter) => c?.position === i)
|
||||
))
|
||||
|
||||
// Get job from party if not directly provided
|
||||
const currentJob = $derived(job || party?.job)
|
||||
const genderValue = $derived(gender !== undefined ? gender : 0) // Default to Gran if not specified
|
||||
|
||||
const protagonistClass = $derived(
|
||||
// If element is directly provided, use it
|
||||
element ? getElementClass(element) :
|
||||
|
|
@ -32,6 +39,11 @@
|
|||
})() : ''
|
||||
)
|
||||
|
||||
// Get job portrait URL if job is available
|
||||
const jobPortraitUrl = $derived(
|
||||
currentJob ? getJobPortraitUrl(currentJob, genderValue as Gender) : ''
|
||||
)
|
||||
|
||||
function characterImageUrl(c?: GridCharacter): string {
|
||||
const id = c?.character?.granblueId
|
||||
if (!id) return ''
|
||||
|
|
@ -58,7 +70,16 @@
|
|||
|
||||
<div class="rep">
|
||||
<ul class="characters">
|
||||
<li class={`protagonist ${protagonistClass}`} class:empty={!protagonistClass}></li>
|
||||
<li class={`protagonist ${protagonistClass}`} class:empty={!currentJob}>
|
||||
{#if currentJob && jobPortraitUrl}
|
||||
<img
|
||||
alt="{currentJob.name.en} job"
|
||||
src={jobPortraitUrl}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
{#each grid as c, i}
|
||||
<li class="character" class:empty={!c}>
|
||||
{#if c}<img
|
||||
|
|
@ -124,6 +145,7 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,15 @@
|
|||
<div class="info">
|
||||
<h2 class:empty={!party.name}>{party.name || '(untitled)'}</h2>
|
||||
<div class="details">
|
||||
<span class={`raid ${!party.raid ? 'empty' : ''}`}
|
||||
>{party.raid ? displayName(party.raid) : 'No raid'}</span
|
||||
>
|
||||
<div class="details-text">
|
||||
<span class={`raid ${!party.raid ? 'empty' : ''}`}
|
||||
>{party.raid ? displayName(party.raid) : 'No raid'}</span
|
||||
>
|
||||
{#if party.job}
|
||||
<span class="separator">•</span>
|
||||
<span class="job">{displayName(party.job)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="pills">
|
||||
{#if party.chargeAttack}
|
||||
|
|
@ -190,12 +196,32 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.details-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: $unit-half;
|
||||
overflow: hidden;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
.separator {
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.job {
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.raid {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
&.empty {
|
||||
color: var(--text-tertiary);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { GridCharacter } from '$lib/types/api/party'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import type { Job } from '$lib/types/api/entities'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import { getCharacterImageWithPose } from '$lib/utils/images'
|
||||
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
|
||||
import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils'
|
||||
import perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg'
|
||||
import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg'
|
||||
|
||||
|
|
@ -16,9 +18,10 @@
|
|||
position: number
|
||||
mainWeaponElement?: number | null
|
||||
partyElement?: number | null
|
||||
job?: Job
|
||||
}
|
||||
|
||||
let { item, position, mainWeaponElement, partyElement }: Props = $props()
|
||||
let { item, position, mainWeaponElement, partyElement, job }: Props = $props()
|
||||
|
||||
type PartyCtx = {
|
||||
getParty: () => Party
|
||||
|
|
@ -43,6 +46,11 @@
|
|||
}
|
||||
// Use $derived to ensure consistent computation between server and client
|
||||
let imageUrl = $derived.by(() => {
|
||||
// For protagonist slot (position 0) with a job, show job portrait
|
||||
if (position === 0 && job) {
|
||||
return getJobPortraitUrl(job, Gender.Gran) // TODO: Get gender from user preferences
|
||||
}
|
||||
|
||||
// If no item or no character with granblueId, return placeholder
|
||||
if (!item || !item.character?.granblueId) {
|
||||
return getCharacterImageWithPose(null, 'main', 0, 0)
|
||||
|
|
@ -58,6 +66,9 @@
|
|||
)
|
||||
})
|
||||
|
||||
// Check if this is the protagonist slot
|
||||
const isProtagonist = $derived(position === 0)
|
||||
|
||||
async function remove() {
|
||||
if (!item?.id) return
|
||||
try {
|
||||
|
|
@ -162,8 +173,9 @@
|
|||
{/if}
|
||||
<img
|
||||
class="image"
|
||||
class:placeholder={!item?.character?.granblueId}
|
||||
alt={displayName(item?.character)}
|
||||
class:placeholder={!item?.character?.granblueId && !isProtagonist}
|
||||
class:protagonist={isProtagonist}
|
||||
alt={isProtagonist && job ? job.name.en : displayName(item?.character)}
|
||||
src={imageUrl}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -189,16 +201,20 @@
|
|||
{#key `empty-${position}`}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit()}
|
||||
class:editable={ctx?.canEdit() && !isProtagonist}
|
||||
class:protagonist={isProtagonist}
|
||||
onclick={() =>
|
||||
!isProtagonist &&
|
||||
ctx?.canEdit() &&
|
||||
ctx?.openPicker &&
|
||||
ctx.openPicker({ type: 'character', position, item })}
|
||||
>
|
||||
<img
|
||||
class="image placeholder"
|
||||
alt=""
|
||||
src="/images/placeholders/placeholder-weapon-grid.png"
|
||||
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()}
|
||||
<span class="icon">
|
||||
|
|
@ -316,6 +332,18 @@
|
|||
&.placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.protagonist {
|
||||
object-fit: cover;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.frame.protagonist {
|
||||
// Protagonist slot may have different aspect ratio for job portraits
|
||||
.image {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
|
|
|||
Loading…
Reference in a new issue