feat: add uncap indicators to units and database
This commit is contained in:
parent
9764c80771
commit
ff711331d4
15 changed files with 1264 additions and 31 deletions
60
src/lib/components/database/UncapCell.svelte
Normal file
60
src/lib/components/database/UncapCell.svelte
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: any
|
||||||
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
}
|
||||||
|
|
||||||
|
let { row, type }: Props = $props()
|
||||||
|
|
||||||
|
// Extract uncap data from row
|
||||||
|
const uncapData = $derived(() => {
|
||||||
|
// Direct properties on the row
|
||||||
|
const uncapLevel = row.uncap_level ?? row.max_level ?? 3
|
||||||
|
const transcendenceStage = row.transcendence_step ?? 0
|
||||||
|
|
||||||
|
// Check for uncap object if it exists
|
||||||
|
const uncap = row.uncap ?? {}
|
||||||
|
const flb = uncap.flb ?? false
|
||||||
|
const ulb = uncap.ulb ?? false
|
||||||
|
const transcendence = uncap.transcendence ?? false
|
||||||
|
|
||||||
|
// Special flag for characters
|
||||||
|
const special = type === 'character' && row.special
|
||||||
|
|
||||||
|
return {
|
||||||
|
uncapLevel,
|
||||||
|
transcendenceStage,
|
||||||
|
flb,
|
||||||
|
ulb,
|
||||||
|
transcendence,
|
||||||
|
special
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="uncap-cell">
|
||||||
|
<UncapIndicator
|
||||||
|
{type}
|
||||||
|
uncapLevel={uncapData().uncapLevel}
|
||||||
|
transcendenceStage={uncapData().transcendenceStage}
|
||||||
|
flb={uncapData().flb}
|
||||||
|
ulb={uncapData().ulb}
|
||||||
|
transcendence={uncapData().transcendence}
|
||||||
|
special={uncapData().special}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.uncap-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
src/lib/components/database/cells/CharacterUncapCell.svelte
Normal file
68
src/lib/components/database/cells/CharacterUncapCell.svelte
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { IRow } from 'wx-svelte-grid'
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: IRow
|
||||||
|
}
|
||||||
|
|
||||||
|
let { row }: Props = $props()
|
||||||
|
|
||||||
|
// For database view, show maximum possible uncap level
|
||||||
|
// Not the user's current uncap level
|
||||||
|
const uncap = $derived(row.uncap ?? {})
|
||||||
|
const flb = $derived(uncap.flb ?? false)
|
||||||
|
const ulb = $derived(uncap.ulb ?? false)
|
||||||
|
const transcendence = $derived(uncap.transcendence ?? false)
|
||||||
|
const special = $derived(row.special ?? false)
|
||||||
|
|
||||||
|
// Calculate maximum uncap level based on available uncaps
|
||||||
|
const getMaxUncapLevel = () => {
|
||||||
|
if (special) {
|
||||||
|
// Special characters: 3 base + FLB + ULB
|
||||||
|
return ulb ? 5 : flb ? 4 : 3
|
||||||
|
} else {
|
||||||
|
// Regular characters: 4 base + FLB + ULB/transcendence
|
||||||
|
return ulb ? 6 : flb ? 5 : 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uncapLevel = $derived(getMaxUncapLevel())
|
||||||
|
// For database view, show maximum transcendence stage when available
|
||||||
|
// Only regular (non-special) characters have transcendence
|
||||||
|
const transcendenceStage = $derived(
|
||||||
|
// Special characters don't have transcendence
|
||||||
|
special ? 0 :
|
||||||
|
// First check if API provides direct transcendence_step field on the row
|
||||||
|
row.transcendence_step ? row.transcendence_step :
|
||||||
|
// Check if API provides specific max transcendence step in uncap object
|
||||||
|
uncap.max_transcendence_step ? uncap.max_transcendence_step :
|
||||||
|
// Otherwise, show maximum stage (5) when transcendence is available for regular characters
|
||||||
|
transcendence ? 5 : 0
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="uncap-cell">
|
||||||
|
<UncapIndicator
|
||||||
|
type="character"
|
||||||
|
{uncapLevel}
|
||||||
|
{transcendenceStage}
|
||||||
|
{flb}
|
||||||
|
{ulb}
|
||||||
|
{transcendence}
|
||||||
|
{special}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.uncap-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
144
src/lib/components/database/cells/LastUpdatedCell.svelte
Normal file
144
src/lib/components/database/cells/LastUpdatedCell.svelte
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from 'svelte'
|
||||||
|
import type { Cell } from 'wx-svelte-grid'
|
||||||
|
|
||||||
|
type Props = ComponentProps<Cell> & {
|
||||||
|
row: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const { row }: Props = $props()
|
||||||
|
|
||||||
|
// Get the most recent date from various date fields
|
||||||
|
const getLastUpdated = (item: any): Date | null => {
|
||||||
|
if (!item) return null
|
||||||
|
|
||||||
|
const dates: Date[] = []
|
||||||
|
|
||||||
|
// Check for date fields in the API response
|
||||||
|
if (item.release_date) {
|
||||||
|
const date = new Date(item.release_date)
|
||||||
|
if (!isNaN(date.getTime())) dates.push(date)
|
||||||
|
}
|
||||||
|
if (item.flb_date) {
|
||||||
|
const date = new Date(item.flb_date)
|
||||||
|
if (!isNaN(date.getTime())) dates.push(date)
|
||||||
|
}
|
||||||
|
if (item.ulb_date) {
|
||||||
|
const date = new Date(item.ulb_date)
|
||||||
|
if (!isNaN(date.getTime())) dates.push(date)
|
||||||
|
}
|
||||||
|
if (item.transcendence_date) {
|
||||||
|
const date = new Date(item.transcendence_date)
|
||||||
|
if (!isNaN(date.getTime())) dates.push(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the most recent date
|
||||||
|
if (dates.length === 0) return null
|
||||||
|
return dates.reduce((latest, current) => (current > latest ? current : latest))
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date: Date | null): string => {
|
||||||
|
if (!date) return '—'
|
||||||
|
|
||||||
|
// Format as YYYY-MM-DD
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastUpdated = $derived(getLastUpdated(row))
|
||||||
|
const formattedDate = $derived(formatDate(lastUpdated))
|
||||||
|
|
||||||
|
// Determine what type of update this was
|
||||||
|
const getUpdateType = (item: any): string => {
|
||||||
|
const lastDate = getLastUpdated(item)
|
||||||
|
if (!lastDate) return ''
|
||||||
|
|
||||||
|
const lastTime = lastDate.getTime()
|
||||||
|
|
||||||
|
// Compare timestamps to determine which date field matches
|
||||||
|
if (item.transcendence_date) {
|
||||||
|
const transcendDate = new Date(item.transcendence_date)
|
||||||
|
if (!isNaN(transcendDate.getTime()) && transcendDate.getTime() === lastTime) {
|
||||||
|
return 'Transcendence'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.ulb_date) {
|
||||||
|
const ulbDate = new Date(item.ulb_date)
|
||||||
|
if (!isNaN(ulbDate.getTime()) && ulbDate.getTime() === lastTime) {
|
||||||
|
// Characters with transcendence have their "ULB" date but it's actually transcendence
|
||||||
|
// Check if this is a character by looking for character-specific fields
|
||||||
|
// Characters have 'race' and 'proficiency' arrays, weapons/summons don't have 'race'
|
||||||
|
const isCharacter = Array.isArray(item.race) && Array.isArray(item.proficiency)
|
||||||
|
return isCharacter ? 'Transcendence' : 'ULB'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.flb_date) {
|
||||||
|
const flbDate = new Date(item.flb_date)
|
||||||
|
if (!isNaN(flbDate.getTime()) && flbDate.getTime() === lastTime) {
|
||||||
|
return 'FLB'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.release_date) {
|
||||||
|
const releaseDate = new Date(item.release_date)
|
||||||
|
if (!isNaN(releaseDate.getTime()) && releaseDate.getTime() === lastTime) {
|
||||||
|
return 'Release'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateType = $derived(getUpdateType(row))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="last-updated-cell">
|
||||||
|
<span class="date">{formattedDate}</span>
|
||||||
|
{#if updateType}
|
||||||
|
<span class="type" data-type={updateType.toLowerCase()}>{updateType}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.last-updated-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: spacing.$unit * 0.25;
|
||||||
|
padding: spacing.$unit * 0.5 0;
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
font-size: typography.$font-tiny;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&[data-type='transcendence'] {
|
||||||
|
color: #9b59b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type='ulb'] {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type='flb'] {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type='release'] {
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
src/lib/components/database/cells/SummonUncapCell.svelte
Normal file
57
src/lib/components/database/cells/SummonUncapCell.svelte
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { IRow } from 'wx-svelte-grid'
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: IRow
|
||||||
|
}
|
||||||
|
|
||||||
|
let { row }: Props = $props()
|
||||||
|
|
||||||
|
// For database view, show maximum possible uncap level
|
||||||
|
const uncap = $derived(row.uncap ?? {})
|
||||||
|
const flb = $derived(uncap.flb ?? false)
|
||||||
|
const ulb = $derived(uncap.ulb ?? false)
|
||||||
|
const transcendence = $derived(uncap.transcendence ?? false)
|
||||||
|
|
||||||
|
// Calculate maximum uncap level based on available uncaps
|
||||||
|
// Summons: 3 base + FLB + ULB + transcendence
|
||||||
|
const getMaxUncapLevel = () => {
|
||||||
|
return transcendence ? 6 : ulb ? 5 : flb ? 4 : 3
|
||||||
|
}
|
||||||
|
|
||||||
|
const uncapLevel = $derived(getMaxUncapLevel())
|
||||||
|
// For database view, show maximum transcendence stage when available
|
||||||
|
const transcendenceStage = $derived(
|
||||||
|
// First check if API provides direct transcendence_step field on the row
|
||||||
|
row.transcendence_step ? row.transcendence_step :
|
||||||
|
// Check if API provides specific max transcendence step in uncap object
|
||||||
|
uncap.max_transcendence_step ? uncap.max_transcendence_step :
|
||||||
|
// Otherwise, show maximum stage (5) when transcendence is available
|
||||||
|
transcendence ? 5 : 0
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="uncap-cell">
|
||||||
|
<UncapIndicator
|
||||||
|
type="summon"
|
||||||
|
{uncapLevel}
|
||||||
|
{transcendenceStage}
|
||||||
|
{flb}
|
||||||
|
{ulb}
|
||||||
|
{transcendence}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.uncap-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
src/lib/components/database/cells/WeaponUncapCell.svelte
Normal file
57
src/lib/components/database/cells/WeaponUncapCell.svelte
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { IRow } from 'wx-svelte-grid'
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: IRow
|
||||||
|
}
|
||||||
|
|
||||||
|
let { row }: Props = $props()
|
||||||
|
|
||||||
|
// For database view, show maximum possible uncap level
|
||||||
|
const uncap = $derived(row.uncap ?? {})
|
||||||
|
const flb = $derived(uncap.flb ?? false)
|
||||||
|
const ulb = $derived(uncap.ulb ?? false)
|
||||||
|
const transcendence = $derived(uncap.transcendence ?? false)
|
||||||
|
|
||||||
|
// Calculate maximum uncap level based on available uncaps
|
||||||
|
// Weapons: 3 base + FLB + ULB + transcendence
|
||||||
|
const getMaxUncapLevel = () => {
|
||||||
|
return transcendence ? 6 : ulb ? 5 : flb ? 4 : 3
|
||||||
|
}
|
||||||
|
|
||||||
|
const uncapLevel = $derived(getMaxUncapLevel())
|
||||||
|
// For database view, show maximum transcendence stage when available
|
||||||
|
const transcendenceStage = $derived(
|
||||||
|
// First check if API provides direct transcendence_step field on the row
|
||||||
|
row.transcendence_step ? row.transcendence_step :
|
||||||
|
// Check if API provides specific max transcendence step in uncap object
|
||||||
|
uncap.max_transcendence_step ? uncap.max_transcendence_step :
|
||||||
|
// Otherwise, show maximum stage (5) when transcendence is available
|
||||||
|
transcendence ? 5 : 0
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="uncap-cell">
|
||||||
|
<UncapIndicator
|
||||||
|
type="weapon"
|
||||||
|
{uncapLevel}
|
||||||
|
{transcendenceStage}
|
||||||
|
{flb}
|
||||||
|
{ulb}
|
||||||
|
{transcendence}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.uncap-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
src/lib/components/uncap/TranscendenceFragment.svelte
Normal file
116
src/lib/components/uncap/TranscendenceFragment.svelte
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
stage: number
|
||||||
|
interactive?: boolean
|
||||||
|
visible?: boolean
|
||||||
|
onClick?: (index: number) => void
|
||||||
|
onHover?: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
stage,
|
||||||
|
interactive = false,
|
||||||
|
visible = false,
|
||||||
|
onClick,
|
||||||
|
onHover
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (interactive && onClick) {
|
||||||
|
onClick(stage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHover() {
|
||||||
|
if (interactive && onHover) {
|
||||||
|
onHover(stage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i
|
||||||
|
class="fragment"
|
||||||
|
class:visible
|
||||||
|
class:stage1={stage === 1}
|
||||||
|
class:stage2={stage === 2}
|
||||||
|
class:stage3={stage === 3}
|
||||||
|
class:stage4={stage === 4}
|
||||||
|
class:stage5={stage === 5}
|
||||||
|
onclick={handleClick}
|
||||||
|
onmouseover={handleHover}
|
||||||
|
role={interactive ? 'button' : undefined}
|
||||||
|
aria-label={interactive ? `Transcendence fragment ${stage}` : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
|
||||||
|
.fragment {
|
||||||
|
--degrees: 72deg;
|
||||||
|
--orig-width: 29px;
|
||||||
|
--orig-height: 54px;
|
||||||
|
--scaled-width: 12px;
|
||||||
|
--scaled-height: calc((var(--scaled-width) / var(--orig-width)) * var(--orig-height));
|
||||||
|
--scale: 1.2;
|
||||||
|
|
||||||
|
background-image: url('/icons/transcendence/interactive/interactive-piece.png');
|
||||||
|
background-size: var(--scaled-width) var(--scaled-height);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 32;
|
||||||
|
|
||||||
|
aspect-ratio: 29 / 54;
|
||||||
|
height: var(--scaled-height);
|
||||||
|
width: var(--scaled-width);
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage1 {
|
||||||
|
top: 3px;
|
||||||
|
left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage2 {
|
||||||
|
top: 10px;
|
||||||
|
left: 27px;
|
||||||
|
transform: rotate(var(--degrees));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage3 {
|
||||||
|
top: 21px;
|
||||||
|
left: 24px;
|
||||||
|
transform: rotate(calc(var(--degrees) * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage4 {
|
||||||
|
top: 21px;
|
||||||
|
left: 12px;
|
||||||
|
transform: rotate(calc(var(--degrees) * 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage5 {
|
||||||
|
top: 10px;
|
||||||
|
left: 8px;
|
||||||
|
transform: rotate(calc(var(--degrees) * 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High DPI support */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
background-image: url('/icons/transcendence/interactive/interactive-piece@2x.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) {
|
||||||
|
background-image: url('/icons/transcendence/interactive/interactive-piece@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
src/lib/components/uncap/TranscendenceStar.svelte
Normal file
237
src/lib/components/uncap/TranscendenceStar.svelte
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import TranscendenceFragment from './TranscendenceFragment.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
stage?: number
|
||||||
|
editable?: boolean
|
||||||
|
interactive?: boolean
|
||||||
|
tabIndex?: number
|
||||||
|
onStarClick?: () => void
|
||||||
|
onFragmentClick?: (newStage: number) => void
|
||||||
|
onFragmentHover?: (newStage: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
className,
|
||||||
|
stage = 0,
|
||||||
|
editable = false,
|
||||||
|
interactive = false,
|
||||||
|
tabIndex,
|
||||||
|
onStarClick,
|
||||||
|
onFragmentClick,
|
||||||
|
onFragmentHover
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
const NUM_FRAGMENTS = 5
|
||||||
|
|
||||||
|
let visibleStage = $state(stage)
|
||||||
|
let currentStage = $state(stage)
|
||||||
|
let immutable = $state(false)
|
||||||
|
let starElement: HTMLDivElement
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
visibleStage = stage
|
||||||
|
currentStage = stage
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (editable && onStarClick) {
|
||||||
|
onStarClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFragmentClick(index: number) {
|
||||||
|
let newStage = index
|
||||||
|
if (index === currentStage) {
|
||||||
|
newStage = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleStage = newStage
|
||||||
|
currentStage = newStage
|
||||||
|
if (onFragmentClick) {
|
||||||
|
onFragmentClick(newStage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFragmentHover(index: number) {
|
||||||
|
visibleStage = index
|
||||||
|
if (onFragmentHover) {
|
||||||
|
onFragmentHover(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
visibleStage = currentStage
|
||||||
|
if (onFragmentHover) {
|
||||||
|
onFragmentHover(currentStage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="star TranscendenceStar"
|
||||||
|
class:immutable
|
||||||
|
class:empty={stage === 0}
|
||||||
|
class:stage1={stage === 1}
|
||||||
|
class:stage2={stage === 2}
|
||||||
|
class:stage3={stage === 3}
|
||||||
|
class:stage4={stage === 4}
|
||||||
|
class:stage5={stage === 5}
|
||||||
|
onclick={handleClick}
|
||||||
|
onmouseleave={interactive ? handleMouseLeave : undefined}
|
||||||
|
bind:this={starElement}
|
||||||
|
{tabIndex}
|
||||||
|
role={editable ? 'button' : undefined}
|
||||||
|
aria-label={editable ? 'Transcendence star' : undefined}
|
||||||
|
>
|
||||||
|
<div class="fragments">
|
||||||
|
{#if interactive}
|
||||||
|
{#each Array(NUM_FRAGMENTS) as _, i}
|
||||||
|
{@const loopStage = i + 1}
|
||||||
|
<TranscendenceFragment
|
||||||
|
stage={loopStage}
|
||||||
|
visible={loopStage <= visibleStage}
|
||||||
|
{interactive}
|
||||||
|
onClick={handleFragmentClick}
|
||||||
|
onHover={handleFragmentHover}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<i class="figure {className || ''}" class:interactive class:base={className?.includes('base')} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
|
||||||
|
.star {
|
||||||
|
--size: 18px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.immutable {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
background-image: url('/icons/transcendence/0/stage-0.png');
|
||||||
|
background-size: var(--size) var(--size);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage1 {
|
||||||
|
background-image: url('/icons/transcendence/1/stage-1.png');
|
||||||
|
background-size: var(--size) var(--size);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage2 {
|
||||||
|
background-image: url('/icons/transcendence/2/stage-2.png');
|
||||||
|
background-size: var(--size) var(--size);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage3 {
|
||||||
|
background-image: url('/icons/transcendence/3/stage-3.png');
|
||||||
|
background-size: var(--size) var(--size);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage4 {
|
||||||
|
background-image: url('/icons/transcendence/4/stage-4.png');
|
||||||
|
background-size: var(--size) var(--size);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stage5 {
|
||||||
|
background-image: url('/icons/transcendence/5/stage-5.png');
|
||||||
|
background-size: var(--size) var(--size);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High DPI support */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
&.empty {
|
||||||
|
background-image: url('/icons/transcendence/0/stage-0@2x.png');
|
||||||
|
}
|
||||||
|
&.stage1 {
|
||||||
|
background-image: url('/icons/transcendence/1/stage-1@2x.png');
|
||||||
|
}
|
||||||
|
&.stage2 {
|
||||||
|
background-image: url('/icons/transcendence/2/stage-2@2x.png');
|
||||||
|
}
|
||||||
|
&.stage3 {
|
||||||
|
background-image: url('/icons/transcendence/3/stage-3@2x.png');
|
||||||
|
}
|
||||||
|
&.stage4 {
|
||||||
|
background-image: url('/icons/transcendence/4/stage-4@2x.png');
|
||||||
|
}
|
||||||
|
&.stage5 {
|
||||||
|
background-image: url('/icons/transcendence/5/stage-5@2x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) {
|
||||||
|
&.empty {
|
||||||
|
background-image: url('/icons/transcendence/0/stage-0@3x.png');
|
||||||
|
}
|
||||||
|
&.stage1 {
|
||||||
|
background-image: url('/icons/transcendence/1/stage-1@3x.png');
|
||||||
|
}
|
||||||
|
&.stage2 {
|
||||||
|
background-image: url('/icons/transcendence/2/stage-2@3x.png');
|
||||||
|
}
|
||||||
|
&.stage3 {
|
||||||
|
background-image: url('/icons/transcendence/3/stage-3@3x.png');
|
||||||
|
}
|
||||||
|
&.stage4 {
|
||||||
|
background-image: url('/icons/transcendence/4/stage-4@3x.png');
|
||||||
|
}
|
||||||
|
&.stage5 {
|
||||||
|
background-image: url('/icons/transcendence/5/stage-5@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.figure {
|
||||||
|
--size: 18px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 54px 54px;
|
||||||
|
display: block;
|
||||||
|
height: var(--size);
|
||||||
|
width: var(--size);
|
||||||
|
|
||||||
|
&.interactive.base {
|
||||||
|
--size: calc(spacing.$unit * 6);
|
||||||
|
background-image: url('/icons/transcendence/interactive/interactive-base.png');
|
||||||
|
background-size: var(--size) var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
width: var(--size);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High DPI support */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
background-image: url('/icons/transcendence/interactive/interactive-base@2x.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) {
|
||||||
|
background-image: url('/icons/transcendence/interactive/interactive-base@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
184
src/lib/components/uncap/UncapIndicator.svelte
Normal file
184
src/lib/components/uncap/UncapIndicator.svelte
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import UncapStar from './UncapStar.svelte'
|
||||||
|
import TranscendenceStar from './TranscendenceStar.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
rarity?: number
|
||||||
|
uncapLevel?: number
|
||||||
|
transcendenceStage?: number
|
||||||
|
flb?: boolean
|
||||||
|
ulb?: boolean
|
||||||
|
transcendence?: boolean
|
||||||
|
special?: boolean
|
||||||
|
className?: string
|
||||||
|
editable?: boolean
|
||||||
|
updateUncap?: (index: number) => void
|
||||||
|
updateTranscendence?: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StarRender {
|
||||||
|
type: 'uncap' | 'transcendence'
|
||||||
|
props: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
type,
|
||||||
|
rarity,
|
||||||
|
uncapLevel = 0,
|
||||||
|
transcendenceStage = 0,
|
||||||
|
flb = false,
|
||||||
|
ulb = false,
|
||||||
|
transcendence = false,
|
||||||
|
special = false,
|
||||||
|
className,
|
||||||
|
editable = false,
|
||||||
|
updateUncap,
|
||||||
|
updateTranscendence
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
// Calculate the total number of stars to display
|
||||||
|
const getNumStars = () => {
|
||||||
|
if (type === 'character') {
|
||||||
|
if (special) {
|
||||||
|
// Special characters: 3 base + FLB + ULB
|
||||||
|
return ulb ? 5 : flb ? 4 : 3
|
||||||
|
} else {
|
||||||
|
// Regular characters: 4 base + FLB + transcendence (ulb flag = transcendence for regular chars)
|
||||||
|
return ulb ? 6 : flb ? 5 : 4
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Weapons and summons: 3 base + FLB + ULB + transcendence
|
||||||
|
return transcendence ? 6 : ulb ? 5 : flb ? 4 : 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numStars = $derived(getNumStars())
|
||||||
|
|
||||||
|
function toggleStar(index: number, empty: boolean) {
|
||||||
|
if (updateUncap && editable) {
|
||||||
|
if (empty && index > 0) {
|
||||||
|
updateUncap(index + 1)
|
||||||
|
} else {
|
||||||
|
updateUncap(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTranscendenceUpdate(stage: number) {
|
||||||
|
if (updateTranscendence && editable) {
|
||||||
|
updateTranscendence(stage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create star props
|
||||||
|
const createStarProps = (starType: 'uncap' | 'transcendence', options: any = {}): StarRender => {
|
||||||
|
if (starType === 'transcendence') {
|
||||||
|
return {
|
||||||
|
type: 'transcendence',
|
||||||
|
props: {
|
||||||
|
stage: transcendenceStage,
|
||||||
|
editable,
|
||||||
|
interactive: editable,
|
||||||
|
onFragmentClick: editable ? handleTranscendenceUpdate : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'uncap',
|
||||||
|
props: {
|
||||||
|
empty: options.index >= uncapLevel,
|
||||||
|
index: options.index,
|
||||||
|
flb: options.flb,
|
||||||
|
ulb: options.ulb,
|
||||||
|
special: options.special,
|
||||||
|
tabIndex: editable ? 0 : undefined,
|
||||||
|
onStarClick: editable ? toggleStar : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine what type of star to render at each position
|
||||||
|
function renderStar(index: number): StarRender | null {
|
||||||
|
// Handle transcendence star (always at position 5)
|
||||||
|
if (index === 5) {
|
||||||
|
if (type === 'character' && !special && transcendence) {
|
||||||
|
// Regular character with transcendence (note: uses transcendence flag, not ulb)
|
||||||
|
return createStarProps('transcendence')
|
||||||
|
}
|
||||||
|
if ((type === 'weapon' || type === 'summon') && transcendence) {
|
||||||
|
// Weapon/summon with transcendence
|
||||||
|
return createStarProps('transcendence')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ULB star
|
||||||
|
if (index === 4) {
|
||||||
|
if (type === 'character' && special && ulb) {
|
||||||
|
// Special character ULB at position 4
|
||||||
|
return createStarProps('uncap', { index, ulb: true })
|
||||||
|
}
|
||||||
|
if (type === 'weapon' && ulb) {
|
||||||
|
// Weapon ULB at position 4 (blue, not purple)
|
||||||
|
return createStarProps('uncap', { index, flb: true })
|
||||||
|
}
|
||||||
|
if (type === 'summon' && ulb) {
|
||||||
|
// Summon ULB at position 4 (blue, not purple)
|
||||||
|
return createStarProps('uncap', { index, flb: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle FLB star
|
||||||
|
if (index === 3) {
|
||||||
|
if ((type === 'weapon' || type === 'summon' || (type === 'character' && special)) && flb) {
|
||||||
|
// Weapon/summon/special character FLB at position 3
|
||||||
|
return createStarProps('uncap', { index, flb: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index === 4 && type === 'character' && !special && flb) {
|
||||||
|
// Regular character FLB at position 4
|
||||||
|
return createStarProps('uncap', { index, flb: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular MLB stars (positions 0-2 for weapons/summons/special chars, 0-3 for regular chars)
|
||||||
|
if (index < 3 || (type === 'character' && !special && index === 3)) {
|
||||||
|
return createStarProps('uncap', { index })
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="uncap-indicator {className || ''}">
|
||||||
|
<ul class="stars">
|
||||||
|
{#each Array(numStars) as _, i}
|
||||||
|
{@const star = renderStar(i)}
|
||||||
|
{#if star}
|
||||||
|
{#if star.type === 'transcendence'}
|
||||||
|
<TranscendenceStar {...star.props} />
|
||||||
|
{:else}
|
||||||
|
<UncapStar {...star.props} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.uncap-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
src/lib/components/uncap/UncapStar.svelte
Normal file
109
src/lib/components/uncap/UncapStar.svelte
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
empty?: boolean
|
||||||
|
special?: boolean
|
||||||
|
flb?: boolean
|
||||||
|
ulb?: boolean
|
||||||
|
index: number
|
||||||
|
tabIndex?: number
|
||||||
|
onStarClick: (index: number, empty: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
empty = false,
|
||||||
|
special = false,
|
||||||
|
flb = false,
|
||||||
|
ulb = false,
|
||||||
|
index,
|
||||||
|
tabIndex,
|
||||||
|
onStarClick
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
onStarClick(index, empty)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
class="star"
|
||||||
|
class:empty
|
||||||
|
class:special
|
||||||
|
class:mlb={!special}
|
||||||
|
class:flb
|
||||||
|
class:ulb
|
||||||
|
{tabIndex}
|
||||||
|
onclick={handleClick}
|
||||||
|
role="button"
|
||||||
|
aria-label="Uncap star"
|
||||||
|
></li>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.star {
|
||||||
|
--size: 18px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: var(--size);
|
||||||
|
display: block;
|
||||||
|
height: var(--size);
|
||||||
|
width: var(--size);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty,
|
||||||
|
&.empty.mlb,
|
||||||
|
&.empty.flb,
|
||||||
|
&.empty.ulb,
|
||||||
|
&.empty.special {
|
||||||
|
background-image: url('/icons/uncap/empty@3x.png');
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-image: url('/icons/uncap/empty-hover@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mlb {
|
||||||
|
background-image: url('/icons/uncap/yellow@3x.png');
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-image: url('/icons/uncap/yellow-hover@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.special {
|
||||||
|
background-image: url('/icons/uncap/red@3x.png');
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-image: url('/icons/uncap/red-hover@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flb {
|
||||||
|
background-image: url('/icons/uncap/blue@3x.png');
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-image: url('/icons/uncap/blue-hover@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ulb {
|
||||||
|
background-image: url('/icons/uncap/purple@3x.png');
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-image: url('/icons/uncap/purple-hover@3x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
--size: 14px;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
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'
|
||||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item?: GridCharacter
|
item?: GridCharacter
|
||||||
|
|
@ -33,15 +34,12 @@
|
||||||
}
|
}
|
||||||
// Use $derived to ensure consistent computation between server and client
|
// Use $derived to ensure consistent computation between server and client
|
||||||
let imageUrl = $derived(() => {
|
let imageUrl = $derived(() => {
|
||||||
// Handle both new structure (item.character) and old structure (item.object) for compatibility
|
// If no item or no character with granblueId, return placeholder
|
||||||
const characterData = item?.character || (item as any)?.object
|
if (!item || !item.character?.granblueId) {
|
||||||
|
|
||||||
// If no item or no granblueId, return placeholder
|
|
||||||
if (!item || !characterData?.granblueId) {
|
|
||||||
return '/images/placeholders/placeholder-weapon-grid.png'
|
return '/images/placeholders/placeholder-weapon-grid.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = characterData.granblueId
|
const id = item.character.granblueId
|
||||||
const uncap = item?.uncapLevel ?? 0
|
const uncap = item?.uncapLevel ?? 0
|
||||||
const transStep = item?.transcendenceStep ?? 0
|
const transStep = item?.transcendenceStep ?? 0
|
||||||
let suffix = '01'
|
let suffix = '01'
|
||||||
|
|
@ -87,7 +85,7 @@
|
||||||
{#if item}
|
{#if item}
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
{#key (item as any).id ?? position}
|
{#key item?.id ?? position}
|
||||||
<div
|
<div
|
||||||
class="frame character cell"
|
class="frame character cell"
|
||||||
class:editable={ctx?.canEdit()}
|
class:editable={ctx?.canEdit()}
|
||||||
|
|
@ -95,8 +93,8 @@
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="image"
|
class="image"
|
||||||
class:placeholder={!(item?.character?.granblueId || (item as any)?.object?.granblueId)}
|
class:placeholder={!item?.character?.granblueId}
|
||||||
alt={displayName(item?.character || (item as any)?.object)}
|
alt={displayName(item?.character)}
|
||||||
src={imageUrl()}
|
src={imageUrl()}
|
||||||
/>
|
/>
|
||||||
{#if ctx?.canEdit() && item?.id}
|
{#if ctx?.canEdit() && item?.id}
|
||||||
|
|
@ -143,7 +141,46 @@
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
<div class="name">{item ? displayName(item?.character || (item as any)?.object) : ''}</div>
|
{#if item}
|
||||||
|
<UncapIndicator
|
||||||
|
type="character"
|
||||||
|
uncapLevel={item.uncapLevel}
|
||||||
|
transcendenceStage={item.transcendenceStep}
|
||||||
|
special={item.character?.special}
|
||||||
|
flb={item.character?.uncap?.flb}
|
||||||
|
ulb={item.character?.uncap?.ulb}
|
||||||
|
editable={ctx?.canEdit()}
|
||||||
|
updateUncap={async (level) => {
|
||||||
|
if (!item?.id || !ctx) return
|
||||||
|
try {
|
||||||
|
const editKey = ctx.getEditKey()
|
||||||
|
const updated = await ctx.services.gridService.updateCharacterUncap(item.id, level, undefined, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update character uncap:', err)
|
||||||
|
// TODO: Show user-friendly error notification
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
updateTranscendence={async (stage) => {
|
||||||
|
if (!item?.id || !ctx) return
|
||||||
|
try {
|
||||||
|
const editKey = ctx.getEditKey()
|
||||||
|
// When setting transcendence > 0, also set uncap to max (6)
|
||||||
|
const maxUncap = stage > 0 ? 6 : undefined
|
||||||
|
const updated = await ctx.services.gridService.updateCharacterUncap(item.id, maxUncap, stage, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update character transcendence:', err)
|
||||||
|
// TODO: Show user-friendly error notification
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="name">{item ? displayName(item?.character) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
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'
|
||||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item?: GridSummon
|
item?: GridSummon
|
||||||
|
|
@ -34,17 +35,14 @@
|
||||||
// Check position first for main/friend summon determination
|
// Check position first for main/friend summon determination
|
||||||
const isMain = position === -1 || position === 6 || item?.main || item?.friend
|
const isMain = position === -1 || position === 6 || item?.main || item?.friend
|
||||||
|
|
||||||
// Handle both new structure (item.summon) and old structure (item.object) for compatibility
|
// If no item or no summon with granblueId, return placeholder
|
||||||
const summonData = item?.summon || (item as any)?.object
|
if (!item || !item.summon?.granblueId) {
|
||||||
|
|
||||||
// If no item or no granblueId, return placeholder
|
|
||||||
if (!item || !summonData?.granblueId) {
|
|
||||||
return isMain
|
return isMain
|
||||||
? '/images/placeholders/placeholder-summon-main.png'
|
? '/images/placeholders/placeholder-summon-main.png'
|
||||||
: '/images/placeholders/placeholder-summon-grid.png'
|
: '/images/placeholders/placeholder-summon-grid.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = summonData.granblueId
|
const id = item.summon.granblueId
|
||||||
const folder = isMain ? 'summon-main' : 'summon-grid'
|
const folder = isMain ? 'summon-main' : 'summon-grid'
|
||||||
return `/images/${folder}/${id}.jpg`
|
return `/images/${folder}/${id}.jpg`
|
||||||
})
|
})
|
||||||
|
|
@ -81,7 +79,7 @@
|
||||||
{#if item}
|
{#if item}
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
{#key (item as any).id ?? position}
|
{#key item?.id ?? position}
|
||||||
<div
|
<div
|
||||||
class="frame summon"
|
class="frame summon"
|
||||||
class:main={item?.main || position === -1}
|
class:main={item?.main || position === -1}
|
||||||
|
|
@ -92,8 +90,8 @@
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="image"
|
class="image"
|
||||||
class:placeholder={!(item?.summon?.granblueId || (item as any)?.object?.granblueId)}
|
class:placeholder={!item?.summon?.granblueId}
|
||||||
alt={displayName(item?.summon || (item as any)?.object)}
|
alt={displayName(item?.summon)}
|
||||||
src={imageUrl()}
|
src={imageUrl()}
|
||||||
/>
|
/>
|
||||||
{#if ctx?.canEdit() && item?.id}
|
{#if ctx?.canEdit() && item?.id}
|
||||||
|
|
@ -149,7 +147,46 @@
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
<div class="name">{item ? displayName(item?.summon || (item as any)?.object) : ''}</div>
|
{#if item}
|
||||||
|
<UncapIndicator
|
||||||
|
type="summon"
|
||||||
|
uncapLevel={item.uncapLevel}
|
||||||
|
transcendenceStage={item.transcendenceStep}
|
||||||
|
flb={item.summon?.uncap?.flb}
|
||||||
|
ulb={item.summon?.uncap?.ulb}
|
||||||
|
transcendence={item.summon?.uncap?.transcendence}
|
||||||
|
editable={ctx?.canEdit()}
|
||||||
|
updateUncap={async (level) => {
|
||||||
|
if (!item?.id || !ctx) return
|
||||||
|
try {
|
||||||
|
const editKey = ctx.getEditKey()
|
||||||
|
const updated = await ctx.services.gridService.updateSummonUncap(item.id, level, undefined, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update summon uncap:', err)
|
||||||
|
// TODO: Show user-friendly error notification
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
updateTranscendence={async (stage) => {
|
||||||
|
if (!item?.id || !ctx) return
|
||||||
|
try {
|
||||||
|
const editKey = ctx.getEditKey()
|
||||||
|
// When setting transcendence > 0, also set uncap to max (6)
|
||||||
|
const maxUncap = stage > 0 ? 6 : undefined
|
||||||
|
const updated = await ctx.services.gridService.updateSummonUncap(item.id, maxUncap, stage, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update summon transcendence:', err)
|
||||||
|
// TODO: Show user-friendly error notification
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="name">{item ? displayName(item?.summon) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
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'
|
||||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item?: GridWeapon
|
item?: GridWeapon
|
||||||
|
|
@ -36,19 +37,16 @@
|
||||||
// Check position first for main weapon determination
|
// Check position first for main weapon determination
|
||||||
const isMain = position === -1 || item?.mainhand
|
const isMain = position === -1 || item?.mainhand
|
||||||
|
|
||||||
// Handle both new structure (item.weapon) and old structure (item.object) for compatibility
|
// If no item or no weapon with granblueId, return placeholder
|
||||||
const weaponData = item?.weapon || (item as any)?.object
|
if (!item || !item.weapon?.granblueId) {
|
||||||
|
|
||||||
// If no item or no granblueId, return placeholder
|
|
||||||
if (!item || !weaponData?.granblueId) {
|
|
||||||
return isMain
|
return isMain
|
||||||
? '/images/placeholders/placeholder-weapon-main.png'
|
? '/images/placeholders/placeholder-weapon-main.png'
|
||||||
: '/images/placeholders/placeholder-weapon-grid.png'
|
: '/images/placeholders/placeholder-weapon-grid.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = weaponData.granblueId
|
const id = item.weapon.granblueId
|
||||||
const folder = isMain ? 'weapon-main' : 'weapon-grid'
|
const folder = isMain ? 'weapon-main' : 'weapon-grid'
|
||||||
const objElement = weaponData?.element
|
const objElement = item.weapon?.element
|
||||||
const instElement = item?.element
|
const instElement = item?.element
|
||||||
|
|
||||||
if (objElement === 0 && instElement) {
|
if (objElement === 0 && instElement) {
|
||||||
|
|
@ -89,7 +87,7 @@
|
||||||
{#if item}
|
{#if item}
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
{#key (item as any).id ?? position}
|
{#key item?.id ?? position}
|
||||||
<div
|
<div
|
||||||
class="frame weapon"
|
class="frame weapon"
|
||||||
class:main={item?.mainhand || position === -1}
|
class:main={item?.mainhand || position === -1}
|
||||||
|
|
@ -98,8 +96,8 @@
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="image"
|
class="image"
|
||||||
class:placeholder={!(item?.weapon?.granblueId || (item as any)?.object?.granblueId)}
|
class:placeholder={!item?.weapon?.granblueId}
|
||||||
alt={displayName(item?.weapon || (item as any)?.object)}
|
alt={displayName(item?.weapon)}
|
||||||
src={imageUrl()}
|
src={imageUrl()}
|
||||||
/>
|
/>
|
||||||
{#if ctx?.canEdit() && item?.id}
|
{#if ctx?.canEdit() && item?.id}
|
||||||
|
|
@ -151,7 +149,46 @@
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
<div class="name">{item ? displayName(item?.weapon || (item as any)?.object) : ''}</div>
|
{#if item}
|
||||||
|
<UncapIndicator
|
||||||
|
type="weapon"
|
||||||
|
uncapLevel={item.uncapLevel}
|
||||||
|
transcendenceStage={item.transcendenceStep}
|
||||||
|
flb={item.weapon?.uncap?.flb}
|
||||||
|
ulb={item.weapon?.uncap?.ulb}
|
||||||
|
transcendence={item.weapon?.uncap?.transcendence}
|
||||||
|
editable={ctx?.canEdit()}
|
||||||
|
updateUncap={async (level) => {
|
||||||
|
if (!item?.id || !ctx) return
|
||||||
|
try {
|
||||||
|
const editKey = ctx.getEditKey()
|
||||||
|
const updated = await ctx.services.gridService.updateWeaponUncap(item.id, level, undefined, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update weapon uncap:', err)
|
||||||
|
// TODO: Show user-friendly error notification
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
updateTranscendence={async (stage) => {
|
||||||
|
if (!item?.id || !ctx) return
|
||||||
|
try {
|
||||||
|
const editKey = ctx.getEditKey()
|
||||||
|
// When setting transcendence > 0, also set uncap to max (6)
|
||||||
|
const maxUncap = stage > 0 ? 6 : undefined
|
||||||
|
const updated = await ctx.services.gridService.updateWeaponUncap(item.id, maxUncap, stage, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update weapon transcendence:', err)
|
||||||
|
// TODO: Show user-friendly error notification
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="name">{item ? displayName(item?.weapon) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
30
src/routes/api/uncap/characters/+server.ts
Normal file
30
src/routes/api/uncap/characters/+server.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API with automatic auth via handleFetch
|
||||||
|
const response = await fetch(buildUrl('/characters/update_uncap'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||||
|
return json(error, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating character uncap:', error)
|
||||||
|
return json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/routes/api/uncap/summons/+server.ts
Normal file
30
src/routes/api/uncap/summons/+server.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API with automatic auth via handleFetch
|
||||||
|
const response = await fetch(buildUrl('/summons/update_uncap'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||||
|
return json(error, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating summon uncap:', error)
|
||||||
|
return json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/routes/api/uncap/weapons/+server.ts
Normal file
30
src/routes/api/uncap/weapons/+server.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API with automatic auth via handleFetch
|
||||||
|
const response = await fetch(buildUrl('/weapons/update_uncap'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||||
|
return json(error, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating weapon uncap:', error)
|
||||||
|
return json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue