components: update party and character components
This commit is contained in:
parent
c3ed9b2885
commit
af659b9760
5 changed files with 62 additions and 216 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
<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/types/party-context'
|
||||
import type { DragDropContext } from '$lib/composables/drag-drop.svelte'
|
||||
|
|
@ -13,7 +12,6 @@
|
|||
characters?: GridCharacter[] | undefined
|
||||
mainWeaponElement?: number | null | undefined
|
||||
partyElement?: number | null | undefined
|
||||
job?: Job | undefined
|
||||
container?: string | undefined
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +19,6 @@
|
|||
characters = [],
|
||||
mainWeaponElement = undefined,
|
||||
partyElement = undefined,
|
||||
job = undefined,
|
||||
container = 'main-characters'
|
||||
}: Props = $props()
|
||||
|
||||
|
|
@ -50,7 +47,6 @@
|
|||
{#each characterSlots as character, i}
|
||||
<li
|
||||
aria-label={`Character slot ${i}`}
|
||||
class:main-character={i === 0}
|
||||
class:Empty={!character}
|
||||
>
|
||||
{#if dragContext}
|
||||
|
|
@ -73,7 +69,6 @@
|
|||
position={i}
|
||||
{mainWeaponElement}
|
||||
{partyElement}
|
||||
job={i === 0 ? job : undefined}
|
||||
/>
|
||||
</DraggableItem>
|
||||
</DropZone>
|
||||
|
|
@ -83,7 +78,6 @@
|
|||
position={i}
|
||||
{mainWeaponElement}
|
||||
{partyElement}
|
||||
job={i === 0 ? job : undefined}
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount, getContext, setContext } from 'svelte'
|
||||
import { onMount, getContext, setContext, onDestroy } from 'svelte'
|
||||
import type { Party, GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||
import { partyStore } from '$lib/stores/partyStore.svelte'
|
||||
|
||||
// TanStack Query mutations - Grid
|
||||
import {
|
||||
|
|
@ -84,6 +85,16 @@
|
|||
initial?.id && initial?.id !== 'new' && Array.isArray(initial?.weapons) ? initial : defaultParty
|
||||
)
|
||||
|
||||
// Sync party to global store for components outside the party context (like DetailsSidebar)
|
||||
$effect(() => {
|
||||
partyStore.setParty(party)
|
||||
})
|
||||
|
||||
// Clear store on unmount
|
||||
onDestroy(() => {
|
||||
partyStore.clear()
|
||||
})
|
||||
|
||||
let activeTab = $state<GridType>(GridType.Weapon)
|
||||
let loading = $state(false)
|
||||
let error = $state<string | null>(null)
|
||||
|
|
@ -249,14 +260,12 @@
|
|||
function handleTabChange(tab: GridType) {
|
||||
activeTab = tab
|
||||
// Update selectedSlot to the first valid empty slot for this tab
|
||||
// Characters tab: position 0 is protagonist (not selectable), so start at 1
|
||||
// Weapons/Summons: start at first empty slot
|
||||
const nextEmpty = findNextEmptySlot(party, tab)
|
||||
if (nextEmpty !== SLOT_NOT_FOUND) {
|
||||
selectedSlot = nextEmpty
|
||||
} else {
|
||||
// Fallback: Characters start at 1 (skip protagonist), others at 0
|
||||
selectedSlot = tab === GridType.Character ? 1 : 0
|
||||
// Fallback: all grid types start at 0
|
||||
selectedSlot = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -902,7 +911,6 @@
|
|||
characters={party.characters}
|
||||
{mainWeaponElement}
|
||||
{partyElement}
|
||||
job={party.job}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { Party, GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||
import { GridType } from '$lib/types/enums'
|
||||
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||
|
|
@ -26,8 +25,6 @@
|
|||
const weapons = $derived(party.weapons)
|
||||
const summons = $derived(party.summons)
|
||||
const characters = $derived(party.characters)
|
||||
const job = $derived(party.job)
|
||||
const element = $derived(party.element)
|
||||
|
||||
// Handle value changes
|
||||
let value = $state(selectedTab)
|
||||
|
|
@ -41,10 +38,6 @@
|
|||
onTabChange?.(newValue as GridType)
|
||||
}
|
||||
|
||||
// Get user gender from context if available
|
||||
// This would typically come from auth/account state
|
||||
const accountContext = getContext<any>('account')
|
||||
const userGender = $derived(accountContext?.user?.gender ?? 0)
|
||||
</script>
|
||||
|
||||
<nav class={className}>
|
||||
|
|
@ -54,12 +47,7 @@
|
|||
label={m.party_segmented_control_characters()}
|
||||
selected={value === GridType.Character}
|
||||
>
|
||||
<CharacterRep
|
||||
job={job}
|
||||
element={element}
|
||||
gender={userGender}
|
||||
characters={characters}
|
||||
/>
|
||||
<CharacterRep characters={characters} />
|
||||
</RepSegment>
|
||||
|
||||
<RepSegment
|
||||
|
|
|
|||
|
|
@ -1,49 +1,21 @@
|
|||
<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/utils/element'
|
||||
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, job, jobId, element, gender }: Props = $props()
|
||||
let { party, characters: directCharacters }: Props = $props()
|
||||
|
||||
// Use direct characters if provided, otherwise get from party
|
||||
const characters = $derived(directCharacters || party?.characters || [])
|
||||
const grid = $derived(Array.from({ length: 3 }, (_, i) =>
|
||||
// Show 5 characters at positions 0-4
|
||||
const grid = $derived(Array.from({ length: 5 }, (_, 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(
|
||||
// If element is directly provided, use it
|
||||
element ? getElementClass(element) :
|
||||
// Otherwise try to get from party's mainhand weapon
|
||||
party ? (() => {
|
||||
const main: GridWeapon | undefined = (party.weapons || []).find(
|
||||
(w: GridWeapon) => w?.mainhand || w?.position === -1
|
||||
)
|
||||
const el = main?.element ?? main?.weapon?.element
|
||||
return getElementClass(el) || ''
|
||||
})() : ''
|
||||
)
|
||||
|
||||
// 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 ''
|
||||
|
|
@ -70,16 +42,6 @@
|
|||
|
||||
<div class="rep">
|
||||
<ul class="characters">
|
||||
<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
|
||||
|
|
@ -96,7 +58,6 @@
|
|||
<style lang="scss">
|
||||
@use '$src/themes/layout' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/rep' as rep;
|
||||
|
||||
.rep {
|
||||
width: 100%;
|
||||
|
|
@ -106,14 +67,13 @@
|
|||
|
||||
.characters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: $unit-half;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
.character,
|
||||
.protagonist {
|
||||
.character {
|
||||
aspect-ratio: 16/33;
|
||||
background: var(--placeholder-bg);
|
||||
border-radius: 4px;
|
||||
|
|
@ -125,62 +85,13 @@
|
|||
background: var(--placeholder-bg);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.character img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.protagonist {
|
||||
border-color: transparent;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
@include rep.aspect(32, 66);
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: var(--wind-portrait-bg);
|
||||
border-color: var(--wind-bg);
|
||||
}
|
||||
|
||||
&.fire {
|
||||
background: var(--fire-portrait-bg);
|
||||
border-color: var(--fire-bg);
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: var(--water-portrait-bg);
|
||||
border-color: var(--water-bg);
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: var(--earth-portrait-bg);
|
||||
border-color: var(--earth-bg);
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: var(--light-portrait-bg);
|
||||
border-color: var(--light-bg);
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: var(--dark-portrait-bg);
|
||||
border-color: var(--dark-bg);
|
||||
}
|
||||
|
||||
&.empty {
|
||||
background: var(--placeholder-bg);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<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 UnitMenuContainer from '$lib/components/ui/menu/UnitMenuContainer.svelte'
|
||||
|
|
@ -9,7 +8,6 @@
|
|||
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 { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import { GridType } from '$lib/types/enums'
|
||||
import perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg'
|
||||
|
|
@ -21,10 +19,9 @@
|
|||
position: number
|
||||
mainWeaponElement?: number | null | undefined
|
||||
partyElement?: number | null | undefined
|
||||
job?: Job | undefined
|
||||
}
|
||||
|
||||
let { item, position, mainWeaponElement, partyElement, job }: Props = $props()
|
||||
let { item, position, mainWeaponElement, partyElement }: Props = $props()
|
||||
|
||||
type PartyCtx = {
|
||||
getParty: () => Party
|
||||
|
|
@ -51,16 +48,6 @@
|
|||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -76,16 +63,12 @@
|
|||
)
|
||||
})
|
||||
|
||||
// Check if this is the protagonist slot
|
||||
const isProtagonist = $derived(position === 0)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
|
@ -194,48 +177,44 @@
|
|||
{#key item?.id ?? position}
|
||||
<div
|
||||
class="frame character cell {elementClass}"
|
||||
class:protagonist={position === 0}
|
||||
class:editable={ctx?.canEdit()}
|
||||
class:is-active={isActive}
|
||||
onclick={() => viewDetails()}
|
||||
>
|
||||
{#if position !== 0}
|
||||
{#if ctx?.canEdit()}
|
||||
<button
|
||||
class="perpetuity"
|
||||
class:active={item.perpetuity}
|
||||
onclick={togglePerpetuity}
|
||||
title={item.perpetuity ? 'Remove Perpetuity Ring' : 'Add Perpetuity Ring'}
|
||||
>
|
||||
<img
|
||||
class="perpetuity-icon filled"
|
||||
src={perpetuityFilled}
|
||||
alt="Perpetuity Ring"
|
||||
/>
|
||||
<img
|
||||
class="perpetuity-icon empty"
|
||||
src={perpetuityEmpty}
|
||||
alt="Add Perpetuity Ring"
|
||||
/>
|
||||
</button>
|
||||
{:else if item.perpetuity}
|
||||
{#if ctx?.canEdit()}
|
||||
<button
|
||||
class="perpetuity"
|
||||
class:active={item.perpetuity}
|
||||
onclick={togglePerpetuity}
|
||||
title={item.perpetuity ? 'Remove Perpetuity Ring' : 'Add Perpetuity Ring'}
|
||||
>
|
||||
<img
|
||||
class="perpetuity static"
|
||||
class="perpetuity-icon filled"
|
||||
src={perpetuityFilled}
|
||||
alt="Perpetuity Ring"
|
||||
title="Perpetuity Ring"
|
||||
/>
|
||||
{/if}
|
||||
<img
|
||||
class="perpetuity-icon empty"
|
||||
src={perpetuityEmpty}
|
||||
alt="Add Perpetuity Ring"
|
||||
/>
|
||||
</button>
|
||||
{:else if item.perpetuity}
|
||||
<img
|
||||
class="perpetuity static"
|
||||
src={perpetuityFilled}
|
||||
alt="Perpetuity Ring"
|
||||
title="Perpetuity Ring"
|
||||
/>
|
||||
{/if}
|
||||
{#if imageUrl}
|
||||
<img
|
||||
class="image {elementClass}"
|
||||
class:placeholder={!item?.character?.granblueId && !isProtagonist}
|
||||
class:protagonist={isProtagonist}
|
||||
alt={isProtagonist && job ? job.name.en : displayName(item?.character)}
|
||||
src={imageUrl}
|
||||
/>
|
||||
{/if}
|
||||
<img
|
||||
class="image {elementClass}"
|
||||
class:placeholder={!item?.character?.granblueId}
|
||||
alt={displayName(item?.character)}
|
||||
src={imageUrl}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
|
@ -271,30 +250,19 @@
|
|||
{#key `empty-${position}`}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit() && !isProtagonist}
|
||||
class:protagonist={isProtagonist}
|
||||
class:empty-protagonist={isProtagonist && !job}
|
||||
class:editable={ctx?.canEdit()}
|
||||
class:is-selected={isEmptySelected}
|
||||
onclick={() =>
|
||||
!isProtagonist &&
|
||||
ctx?.canEdit() &&
|
||||
ctx?.openPicker &&
|
||||
ctx.openPicker({ type: 'character', position, item })}
|
||||
>
|
||||
{#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}
|
||||
<img
|
||||
class="image placeholder"
|
||||
alt=""
|
||||
src="/images/placeholders/placeholder-weapon-grid.png"
|
||||
/>
|
||||
{#if ctx?.canEdit()}
|
||||
<span class="icon">
|
||||
<Icon name="plus" size={24} />
|
||||
</span>
|
||||
|
|
@ -428,17 +396,6 @@
|
|||
|
||||
.frame.character.cell {
|
||||
@include rep.aspect(rep.$char-cell-w, rep.$char-cell-h);
|
||||
|
||||
&.protagonist {
|
||||
background-image: url('/images/relief.png'), linear-gradient(to right, #000, #484440, #000);
|
||||
background-size: cover;
|
||||
background-position: center -20px;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&.empty-protagonist {
|
||||
background-position: center 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
|
|
@ -453,18 +410,6 @@
|
|||
&.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