update grid and unit components

This commit is contained in:
Justin Edmund 2025-09-29 23:47:45 -07:00
parent a00b8a8d18
commit 6b40a3dec6
4 changed files with 107 additions and 16 deletions

View file

@ -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}

View file

@ -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 {

View file

@ -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);

View file

@ -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 {