import type { MobEffectInstance, NbtTag } from 'deepslate'
import { ItemStack, NbtCompound, NbtList, PotionContents } from 'deepslate'
import { Identifier } from 'deepslate/core'
import type { ResolvedItem } from '../services/ResolvedItem.js'
import { makeDescriptionId, mergeTextComponentStyles } from '../Utils.js'
import { TextComponent } from './TextComponent.jsx'
interface Props {
item: ResolvedItem,
advanced?: boolean,
resolver: (item: ItemStack) => ResolvedItem,
}
export function ItemTooltip({ item, advanced, resolver }: Props) {
if (item.has('hide_tooltip')) {
return <>>
}
return <>
{!advanced && !item.has('custom_name') && item.is('filled_map') && item.has('map_id') && (
tag.getAsNumber())], color: 'gray' }} />
)}
{!item.has('hide_additional_tooltip') && <>
{item.is('filled_map') && advanced && (item.get('map_id', tag => tag.isNumber())
? tag.getAsNumber())], color: 'gray' }} />
:
)}
{(item.id.path.endsWith('_banner') || item.is('shield')) && item.get('banner_patterns', tag => tag.isList() ? tag : [])?.map(layer =>
)}
{item.is('crossbow') && item.getChargedProjectile() && (
)}
{item.is('disc_fragment_5') && (
)}
{item.is('firework_rocket') && item.has('fireworks') && <>
{((item.get('fireworks', tag => tag.isCompound() ? tag.getNumber('flight_duration') : 0) ?? 0) > 0) && (
tag.isCompound() ? tag.getNumber('flight_duration') : 0)], color: 'gray'}} />
)}
{/* TODO: firework explosions */}
>}
{item.is('firework_star') && item.has('firework_explosion') && (
tag.isCompound() ? tag.getString('shape') : '')}`, color: 'gray' }} />
// TODO: additional stuff
)}
{/* TODO: painting variants */}
{item.is('goat_horn') && item.has('instrument') && (
tag.isCompound()
? tag.get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('instrument', Identifier.parse(tag.getAsString()))}
), { color: 'gray' })} />
)}
{(item.is('lingering_potion') || item.is('potion') || item.is('splash_potion') || item.is('tipped_arrow')) && (
tag) ?? NbtCompound.create())} factor={item.is('lingering_potion') ? 0.25 : item.is('tipped_arrow') ? 0.125 : 1} />
)}
{/* TODO: mob buckets */}
{/* TODO: smithing templates */}
{item.is('written_book') && item.has('written_book_content') && <>
tag.isCompound() ? tag.getString('author') : undefined) ?? ''], color: 'gray' }} />
tag.isCompound() ? tag.getNumber('generation') : undefined) ?? 0}`, color: 'gray' }} />
>}
{(item.is('beehive') || item.is('bee_nest')) && <>
tag.isList() ? tag.length : 0) ?? 0, 3], color: 'gray' }} />
tag.isCompound() ? tag.getString('honey_level') : 0) ?? 0, 5], color: 'gray' }} />
>}
{item.is('decorated_pot') && item.has('pot_decorations') && <>
{item.get('pot_decorations', tag => tag.isList() ? tag.map(e =>
) : undefined)}
>}
{item.id.path.endsWith('_shulker_box') && <>
{item.has('container_loot') && (
)}
{(item.get('container', tag => tag.isList() ? tag.getItems() : []) ?? []).slice(0, 5).map(e => {
const subItem = resolver(ItemStack.fromNbt(e.isCompound() ? e.getCompound('item') : new NbtCompound()))
return
})}
{(item.get('container', tag => tag.isList() ? tag.length : 0) ?? 0) > 5 && (
tag.isList() ? tag.length : 0) ?? 0) - 5], italic: true }} />
)}
>}
{/* TODO: spawner and trial spawner */}
>}
{item.showInTooltip('jukebox_playable') && <>
tag.isCompound() ? (
tag.hasCompound('song')
? tag.getCompound('song').get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('jukebox_song', Identifier.parse(tag.getString('song')))}
) : {}) ?? {}, { color: 'gray'})} />
>}
{item.showInTooltip('trim') && <>
tag.isCompound() ? (
tag.hasCompound('pattern')
? tag.getCompound('pattern').get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('trim_pattern', Identifier.parse(tag.getString('pattern'))), color: BUILTIN_TRIM_MATERIALS[tag.getString('material').replace(/^minecraft:/, '')] ?? 'gray' }
) : '')] }} />
tag.isCompound() ? (
tag.hasCompound('material')
? tag.getCompound('material').get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('trim_material', Identifier.parse(tag.getString('material'))), color: BUILTIN_TRIM_MATERIALS[tag.getString('material').replace(/^minecraft:/, '')] ?? 'gray' }
) : '')] }}/>
>}
{item.showInTooltip('stored_enchantments') && (
tag)} />
)}
{item.showInTooltip('enchantments') && (
tag)} />
)}
{item.showInTooltip('dyed_color') && (advanced
? tag.isCompound() ? tag.getNumber('rgb') : tag.getAsNumber())?.toString(16).padStart(6, '0')}`], color: 'gray' }} />
:
)}
{item.getLore().map((component) =>
)}
{item.showInTooltip('attribute_modifiers') && (
tag)} />
)}
{item.showInTooltip('unbreakable') && (
)}
{item.has('ominous_bottle_amplifier') && (
tag.getAsNumber()) ?? 0, duration: 120000 }]}} />
)}
{/* TODO: creative-only suspicious stew effects */}
{/* TODO: can break and can place on */}
{advanced && item.isDamageable() && (
)}
{advanced && <>
{item.getSize() > 0 && }
>}
>
}
const BUILTIN_TRIM_MATERIALS: Record = {
amethyst: '#9A5CC6',
copper: '#B4684D',
diamond: '#6EECD2',
emerald: '#11A036',
gold: '#DEB12D',
iron: '#ECECEC',
lapis: '#416E97',
netherite: '#625859',
quartz: '#E3D4C4',
redstone: '#971607',
}
const HARMFUL_EFFECTS = new Set([
'minecraft:slowness',
'minecraft:mining_fatigue',
'minecraft:instant_damage',
'minecraft:nausea',
'minecraft:blindness',
'minecraft:hunger',
'minecraft:weakness',
'minecraft:poison',
'minecraft:wither',
'minecraft:levitation',
'minecraft:unluck',
'minecraft:darkness',
'minecraft:wind_charged',
'minecraft:weaving',
'minecraft:oozing',
'minecraft:infested',
])
function PotionContentsTooltip({ contents, factor }: { contents: PotionContents, factor?: number }) {
const effects = PotionContents.getAllEffects(contents)
return <>
{effects.map(e => {
const color = HARMFUL_EFFECTS.has(e.effect.toString()) ? 'red' : 'blue'
let component: any = { translate: makeDescriptionId('effect', e.effect) }
if (e.amplifier > 0) {
component = { translate: 'potion.withAmplifier', with: [component, { translate: `potion.potency.${e.amplifier}` }] }
}
if (e.duration === -1 || e.duration > 20) {
component = { translate: 'potion.withDuration', with: [component, formatDuration(e, factor ?? 1)] }
}
return
})}
{effects.length === 0 && }
>
}
function formatDuration(effect: MobEffectInstance, factor: number) {
if (effect.duration === -1) {
return { translate: 'effect.duration.infinite' }
}
const ticks = Math.floor(effect.duration * factor)
let seconds = Math.floor(ticks / 20)
let minutes = Math.floor(seconds / 60)
seconds %= 60
const hours = Math.floor(minutes / 60)
minutes %= 60
return `${hours > 0 ? `${hours}:` : ''}${minutes.toFixed().padStart(2, '0')}:${seconds.toFixed().padStart(2, '0')}`
}
function EnchantmentsTooltip({ data }: { data: NbtTag | undefined }) {
if (!data || !data.isCompound()) {
return <>>
}
const levels = data.hasCompound('levels') ? data.getCompound('levels') : data
return <>
{[...levels.keys()].map((key) => {
const level = levels.getNumber(key)
if (level <= 0) return <>>
const id = Identifier.parse(key)
return
})}
>
}
const EQUIPMENT_GROUPS = [
'any',
'mainhand',
'offhand',
'hand',
'feet',
'legs',
'chest',
'head',
'armor',
'body',
]
const MODIFIER_OPERATIONS = [
'add_value',
'add_multiplied_base',
'add_multiplied_total',
]
const NEGATIVE_ATTRIBUTES = new Set([
'minecraft:burning_time',
'minecraft:fall_damage_multiplier',
])
const NEUTRAL_ATTRIBUTES = new Set([
'minecraft:gravity',
'minecraft:scale',
])
function AttributeModifiersTooltip({ data }: { data: NbtTag | undefined }) {
const modifiers = data?.isList() ? data : data?.isCompound() ? data.getList('modifiers') : new NbtList()
return <>
{EQUIPMENT_GROUPS.map(group => {
let first = true
return modifiers.map((e) => {
if (!e.isCompound()) return
const slot = e.has('slot') ? e.getString('slot') : 'any'
if (slot !== group) return
const wasFirst = first
first = false
let amount = e.getNumber('amount')
const type = Identifier.parse(e.getString('type'))
const id = Identifier.parse(e.getString('id'))
const operation = MODIFIER_OPERATIONS.indexOf(e.getString('operation'))
let absolute = false
if (id.equals(Identifier.create('base_attack_damage'))) {
amount += 1
absolute = true
} else if (id.equals(Identifier.create('base_attack_speed'))) {
amount += 4
absolute = true
}
if (operation !== 0) {
amount *= 100
} else if (type.equals(Identifier.create('knockback_resistance'))) {
amount *= 10
}
return <>
{wasFirst && <>
>}
{absolute ? (
) : amount > 0 ? (
) : amount < 0 ? (
) : <>>}
>
})
})}
>
}