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"> <script lang="ts">
import type { GridCharacter } from '$lib/types/api/party' import type { GridCharacter } from '$lib/types/api/party'
import type { Job } from '$lib/types/api/entities'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import type { PartyContext } from '$lib/services/party.service' import type { PartyContext } from '$lib/services/party.service'
import type { DragDropContext } from '$lib/composables/drag-drop.svelte' import type { DragDropContext } from '$lib/composables/drag-drop.svelte'
@ -12,6 +13,7 @@
characters?: GridCharacter[] characters?: GridCharacter[]
mainWeaponElement?: number | null | undefined mainWeaponElement?: number | null | undefined
partyElement?: number | null | undefined partyElement?: number | null | undefined
job?: Job
container?: string container?: string
} }
@ -19,6 +21,7 @@
characters = [], characters = [],
mainWeaponElement = undefined, mainWeaponElement = undefined,
partyElement = undefined, partyElement = undefined,
job = undefined,
container = 'main-characters' container = 'main-characters'
}: Props = $props() }: Props = $props()
@ -65,11 +68,23 @@
type="character" type="character"
canDrag={!!character && (ctx?.canEdit() ?? false)} 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> </DraggableItem>
</DropZone> </DropZone>
{:else} {:else}
<CharacterUnit item={character} position={i} {mainWeaponElement} {partyElement} /> <CharacterUnit
item={character}
position={i}
{mainWeaponElement}
{partyElement}
job={i === 0 ? job : undefined}
/>
{/if} {/if}
</li> </li>
{/each} {/each}

View file

@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { Party, GridWeapon, GridCharacter } from '$lib/types/api/party' 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 { getElementClass } from '$lib/types/enums'
import { getCharacterImageWithPose } from '$lib/utils/images' import { getCharacterImageWithPose } from '$lib/utils/images'
import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils'
interface Props { interface Props {
party?: Party party?: Party
characters?: GridCharacter[] characters?: GridCharacter[]
job?: Job
jobId?: string jobId?: string
element?: number element?: number
gender?: 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 // Use direct characters if provided, otherwise get from party
const characters = $derived(directCharacters || party?.characters || []) const characters = $derived(directCharacters || party?.characters || [])
@ -19,6 +22,10 @@
characters.find((c: GridCharacter) => c?.position === i) 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( const protagonistClass = $derived(
// If element is directly provided, use it // If element is directly provided, use it
element ? getElementClass(element) : 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 { function characterImageUrl(c?: GridCharacter): string {
const id = c?.character?.granblueId const id = c?.character?.granblueId
if (!id) return '' if (!id) return ''
@ -58,7 +70,16 @@
<div class="rep"> <div class="rep">
<ul class="characters"> <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} {#each grid as c, i}
<li class="character" class:empty={!c}> <li class="character" class:empty={!c}>
{#if c}<img {#if c}<img
@ -124,6 +145,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover;
} }
&.wind { &.wind {

View file

@ -31,9 +31,15 @@
<div class="info"> <div class="info">
<h2 class:empty={!party.name}>{party.name || '(untitled)'}</h2> <h2 class:empty={!party.name}>{party.name || '(untitled)'}</h2>
<div class="details"> <div class="details">
<span class={`raid ${!party.raid ? 'empty' : ''}`} <div class="details-text">
>{party.raid ? displayName(party.raid) : 'No raid'}</span <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"> <div class="pills">
{#if party.chargeAttack} {#if party.chargeAttack}
@ -190,12 +196,32 @@
min-width: 0; 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 { .raid {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
flex: 0 1 auto;
min-width: 0;
&.empty { &.empty {
color: var(--text-tertiary); color: var(--text-tertiary);

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { GridCharacter } from '$lib/types/api/party' import type { GridCharacter } from '$lib/types/api/party'
import type { Party } 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 { getContext } from 'svelte'
import Icon from '$lib/components/Icon.svelte' import Icon from '$lib/components/Icon.svelte'
import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
@ -8,6 +9,7 @@
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getCharacterImageWithPose } from '$lib/utils/images' import { getCharacterImageWithPose } from '$lib/utils/images'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte' 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 perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg'
import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg' import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg'
@ -16,9 +18,10 @@
position: number position: number
mainWeaponElement?: number | null mainWeaponElement?: number | null
partyElement?: number | null partyElement?: number | null
job?: Job
} }
let { item, position, mainWeaponElement, partyElement }: Props = $props() let { item, position, mainWeaponElement, partyElement, job }: Props = $props()
type PartyCtx = { type PartyCtx = {
getParty: () => Party getParty: () => Party
@ -43,6 +46,11 @@
} }
// Use $derived to ensure consistent computation between server and client // Use $derived to ensure consistent computation between server and client
let imageUrl = $derived.by(() => { 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 no item or no character with granblueId, return placeholder
if (!item || !item.character?.granblueId) { if (!item || !item.character?.granblueId) {
return getCharacterImageWithPose(null, 'main', 0, 0) 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() { async function remove() {
if (!item?.id) return if (!item?.id) return
try { try {
@ -162,8 +173,9 @@
{/if} {/if}
<img <img
class="image" class="image"
class:placeholder={!item?.character?.granblueId} class:placeholder={!item?.character?.granblueId && !isProtagonist}
alt={displayName(item?.character)} class:protagonist={isProtagonist}
alt={isProtagonist && job ? job.name.en : displayName(item?.character)}
src={imageUrl} src={imageUrl}
/> />
</div> </div>
@ -189,16 +201,20 @@
{#key `empty-${position}`} {#key `empty-${position}`}
<div <div
class="frame character cell" class="frame character cell"
class:editable={ctx?.canEdit()} class:editable={ctx?.canEdit() && !isProtagonist}
class:protagonist={isProtagonist}
onclick={() => onclick={() =>
!isProtagonist &&
ctx?.canEdit() && ctx?.canEdit() &&
ctx?.openPicker && ctx?.openPicker &&
ctx.openPicker({ type: 'character', position, item })} ctx.openPicker({ type: 'character', position, item })}
> >
<img <img
class="image placeholder" class="image"
alt="" class:placeholder={!isProtagonist || !job}
src="/images/placeholders/placeholder-weapon-grid.png" class:protagonist={isProtagonist}
alt={isProtagonist && job ? job.name.en : ""}
src={isProtagonist ? imageUrl : "/images/placeholders/placeholder-weapon-grid.png"}
/> />
{#if ctx?.canEdit()} {#if ctx?.canEdit()}
<span class="icon"> <span class="icon">
@ -316,6 +332,18 @@
&.placeholder { &.placeholder {
opacity: 0; 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 { .icon {