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">
|
<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}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
<div class="details-text">
|
||||||
<span class={`raid ${!party.raid ? 'empty' : ''}`}
|
<span class={`raid ${!party.raid ? 'empty' : ''}`}
|
||||||
>{party.raid ? displayName(party.raid) : 'No raid'}</span
|
>{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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue