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 ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
|
||||
interface Props {
|
||||
item?: GridCharacter
|
||||
|
|
@ -33,15 +34,12 @@
|
|||
}
|
||||
// Use $derived to ensure consistent computation between server and client
|
||||
let imageUrl = $derived(() => {
|
||||
// Handle both new structure (item.character) and old structure (item.object) for compatibility
|
||||
const characterData = item?.character || (item as any)?.object
|
||||
|
||||
// If no item or no granblueId, return placeholder
|
||||
if (!item || !characterData?.granblueId) {
|
||||
// If no item or no character with granblueId, return placeholder
|
||||
if (!item || !item.character?.granblueId) {
|
||||
return '/images/placeholders/placeholder-weapon-grid.png'
|
||||
}
|
||||
|
||||
const id = characterData.granblueId
|
||||
const id = item.character.granblueId
|
||||
const uncap = item?.uncapLevel ?? 0
|
||||
const transStep = item?.transcendenceStep ?? 0
|
||||
let suffix = '01'
|
||||
|
|
@ -87,7 +85,7 @@
|
|||
{#if item}
|
||||
<ContextMenu>
|
||||
{#snippet children()}
|
||||
{#key (item as any).id ?? position}
|
||||
{#key item?.id ?? position}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit()}
|
||||
|
|
@ -95,8 +93,8 @@
|
|||
>
|
||||
<img
|
||||
class="image"
|
||||
class:placeholder={!(item?.character?.granblueId || (item as any)?.object?.granblueId)}
|
||||
alt={displayName(item?.character || (item as any)?.object)}
|
||||
class:placeholder={!item?.character?.granblueId}
|
||||
alt={displayName(item?.character)}
|
||||
src={imageUrl()}
|
||||
/>
|
||||
{#if ctx?.canEdit() && item?.id}
|
||||
|
|
@ -143,7 +141,46 @@
|
|||
</div>
|
||||
{/key}
|
||||
{/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>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import Icon from '$lib/components/Icon.svelte'
|
||||
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
|
||||
interface Props {
|
||||
item?: GridSummon
|
||||
|
|
@ -34,17 +35,14 @@
|
|||
// Check position first for main/friend summon determination
|
||||
const isMain = position === -1 || position === 6 || item?.main || item?.friend
|
||||
|
||||
// Handle both new structure (item.summon) and old structure (item.object) for compatibility
|
||||
const summonData = item?.summon || (item as any)?.object
|
||||
|
||||
// If no item or no granblueId, return placeholder
|
||||
if (!item || !summonData?.granblueId) {
|
||||
// If no item or no summon with granblueId, return placeholder
|
||||
if (!item || !item.summon?.granblueId) {
|
||||
return isMain
|
||||
? '/images/placeholders/placeholder-summon-main.png'
|
||||
: '/images/placeholders/placeholder-summon-grid.png'
|
||||
}
|
||||
|
||||
const id = summonData.granblueId
|
||||
const id = item.summon.granblueId
|
||||
const folder = isMain ? 'summon-main' : 'summon-grid'
|
||||
return `/images/${folder}/${id}.jpg`
|
||||
})
|
||||
|
|
@ -81,7 +79,7 @@
|
|||
{#if item}
|
||||
<ContextMenu>
|
||||
{#snippet children()}
|
||||
{#key (item as any).id ?? position}
|
||||
{#key item?.id ?? position}
|
||||
<div
|
||||
class="frame summon"
|
||||
class:main={item?.main || position === -1}
|
||||
|
|
@ -92,8 +90,8 @@
|
|||
>
|
||||
<img
|
||||
class="image"
|
||||
class:placeholder={!(item?.summon?.granblueId || (item as any)?.object?.granblueId)}
|
||||
alt={displayName(item?.summon || (item as any)?.object)}
|
||||
class:placeholder={!item?.summon?.granblueId}
|
||||
alt={displayName(item?.summon)}
|
||||
src={imageUrl()}
|
||||
/>
|
||||
{#if ctx?.canEdit() && item?.id}
|
||||
|
|
@ -149,7 +147,46 @@
|
|||
</div>
|
||||
{/key}
|
||||
{/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>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import Icon from '$lib/components/Icon.svelte'
|
||||
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
|
||||
interface Props {
|
||||
item?: GridWeapon
|
||||
|
|
@ -36,19 +37,16 @@
|
|||
// Check position first for main weapon determination
|
||||
const isMain = position === -1 || item?.mainhand
|
||||
|
||||
// Handle both new structure (item.weapon) and old structure (item.object) for compatibility
|
||||
const weaponData = item?.weapon || (item as any)?.object
|
||||
|
||||
// If no item or no granblueId, return placeholder
|
||||
if (!item || !weaponData?.granblueId) {
|
||||
// If no item or no weapon with granblueId, return placeholder
|
||||
if (!item || !item.weapon?.granblueId) {
|
||||
return isMain
|
||||
? '/images/placeholders/placeholder-weapon-main.png'
|
||||
: '/images/placeholders/placeholder-weapon-grid.png'
|
||||
}
|
||||
|
||||
const id = weaponData.granblueId
|
||||
const id = item.weapon.granblueId
|
||||
const folder = isMain ? 'weapon-main' : 'weapon-grid'
|
||||
const objElement = weaponData?.element
|
||||
const objElement = item.weapon?.element
|
||||
const instElement = item?.element
|
||||
|
||||
if (objElement === 0 && instElement) {
|
||||
|
|
@ -89,7 +87,7 @@
|
|||
{#if item}
|
||||
<ContextMenu>
|
||||
{#snippet children()}
|
||||
{#key (item as any).id ?? position}
|
||||
{#key item?.id ?? position}
|
||||
<div
|
||||
class="frame weapon"
|
||||
class:main={item?.mainhand || position === -1}
|
||||
|
|
@ -98,8 +96,8 @@
|
|||
>
|
||||
<img
|
||||
class="image"
|
||||
class:placeholder={!(item?.weapon?.granblueId || (item as any)?.object?.granblueId)}
|
||||
alt={displayName(item?.weapon || (item as any)?.object)}
|
||||
class:placeholder={!item?.weapon?.granblueId}
|
||||
alt={displayName(item?.weapon)}
|
||||
src={imageUrl()}
|
||||
/>
|
||||
{#if ctx?.canEdit() && item?.id}
|
||||
|
|
@ -151,7 +149,46 @@
|
|||
</div>
|
||||
{/key}
|
||||
{/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>
|
||||
|
||||
<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