mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
297 lines
14 KiB
TypeScript
297 lines
14 KiB
TypeScript
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 <>
|
|
<TextComponent component={item.getStyledHoverName()} />
|
|
{!advanced && !item.has('custom_name') && item.is('filled_map') && item.has('map_id') && (
|
|
<TextComponent component={{ translate: 'filled_map.id', with: [item.get('map_id', tag => tag.getAsNumber())], color: 'gray' }} />
|
|
)}
|
|
{!item.has('hide_additional_tooltip') && <>
|
|
{item.is('filled_map') && advanced && (item.get('map_id', tag => tag.isNumber())
|
|
? <TextComponent component={{ translate: 'filled_map.id', with: [item.get('map_id', tag => tag.getAsNumber())], color: 'gray' }} />
|
|
: <TextComponent component={{ translate: 'filled_map.unknown', color: 'gray' }} />
|
|
)}
|
|
{(item.id.path.endsWith('_banner') || item.is('shield')) && item.get('banner_patterns', tag => tag.isList() ? tag : [])?.map(layer =>
|
|
<TextComponent component={{ translate: `${layer.isCompound() ? (layer.hasCompound('pattern') ? layer.getString('translation_key') : `block.minecraft.banner.${layer.getString('pattern').replace(/^minecraft:/, '')}`) : ''}.${layer.isCompound() ? layer.getString('color') : ''}`, color: 'gray' }} />
|
|
)}
|
|
{item.is('crossbow') && item.getChargedProjectile() && (
|
|
<TextComponent component={{ translate: 'item.minecraft.crossbow.projectile', extra: [' ', resolver(item.getChargedProjectile()!).getDisplayName()] }}/>
|
|
)}
|
|
{item.is('disc_fragment_5') && (
|
|
<TextComponent component={{ translate: `${makeDescriptionId('item', item.id)}.desc`, color: 'gray' }} />
|
|
)}
|
|
{item.is('firework_rocket') && item.has('fireworks') && <>
|
|
{((item.get('fireworks', tag => tag.isCompound() ? tag.getNumber('flight_duration') : 0) ?? 0) > 0) && (
|
|
<TextComponent component={{ translate: 'item.minecraft.firework_rocket.flight', extra: [' ', item.get('fireworks', tag => tag.isCompound() ? tag.getNumber('flight_duration') : 0)], color: 'gray'}} />
|
|
)}
|
|
{/* TODO: firework explosions */}
|
|
</>}
|
|
{item.is('firework_star') && item.has('firework_explosion') && (
|
|
<TextComponent component={{ translate: `item.minecraft.firework_star.shape.${item.get('firework_explosion', tag => tag.isCompound() ? tag.getString('shape') : '')}`, color: 'gray' }} />
|
|
// TODO: additional stuff
|
|
)}
|
|
{/* TODO: painting variants */}
|
|
{item.is('goat_horn') && item.has('instrument') && (
|
|
<TextComponent component={mergeTextComponentStyles(item.get('instrument', tag => 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')) && (
|
|
<PotionContentsTooltip contents={PotionContents.fromNbt(item.get('potion_contents', tag => 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') && <>
|
|
<TextComponent component={{ translate: 'book.byAuthor', with: [item.get('written_book_content', tag => tag.isCompound() ? tag.getString('author') : undefined) ?? ''], color: 'gray' }} />
|
|
<TextComponent component={{ translate: `book.generation.${item.get('written_book_content', tag => tag.isCompound() ? tag.getNumber('generation') : undefined) ?? 0}`, color: 'gray' }} />
|
|
</>}
|
|
{(item.is('beehive') || item.is('bee_nest')) && <>
|
|
<TextComponent component={{ translate: 'container.beehive.bees', with: [item.get('bees', tag => tag.isList() ? tag.length : 0) ?? 0, 3], color: 'gray' }} />
|
|
<TextComponent component={{ translate: 'container.beehive.honey', with: [item.get('block_state', tag => tag.isCompound() ? tag.getString('honey_level') : 0) ?? 0, 5], color: 'gray' }} />
|
|
</>}
|
|
{item.is('decorated_pot') && item.has('pot_decorations') && <>
|
|
<TextComponent component={''} />
|
|
{item.get('pot_decorations', tag => tag.isList() ? tag.map(e =>
|
|
<TextComponent component={mergeTextComponentStyles(resolver(new ItemStack(Identifier.parse(e.getAsString()), 1)).getHoverName(), { color: 'gray' })} />
|
|
) : undefined)}
|
|
</>}
|
|
{item.id.path.endsWith('_shulker_box') && <>
|
|
{item.has('container_loot') && (
|
|
<TextComponent component={{ translate: 'container.shulkerBox.unknownContents' }} />
|
|
)}
|
|
{(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 <TextComponent component={{ translate: 'container.shulkerBox.itemCount', with: [subItem.getHoverName(), subItem.count] }} />
|
|
})}
|
|
{(item.get('container', tag => tag.isList() ? tag.length : 0) ?? 0) > 5 && (
|
|
<TextComponent component={{ translate: 'container.shulkerBox.more', with: [(item.get('container', tag => tag.isList() ? tag.length : 0) ?? 0) - 5], italic: true }} />
|
|
)}
|
|
</>}
|
|
{/* TODO: spawner and trial spawner */}
|
|
</>}
|
|
{item.showInTooltip('jukebox_playable') && <>
|
|
<TextComponent component={mergeTextComponentStyles(item.get('jukebox_playable', tag => 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') && <>
|
|
<TextComponent component={{ translate: makeDescriptionId('item', Identifier.create('smithing_template.upgrade' )), color: 'gray' }} />
|
|
<TextComponent component={{ text: ' ', extra: [item.get('trim', tag => 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' }
|
|
) : '')] }} />
|
|
<TextComponent component={{ text: ' ', extra: [item.get('trim', tag => 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') && (
|
|
<EnchantmentsTooltip data={item.get('stored_enchantments', tag => tag)} />
|
|
)}
|
|
{item.showInTooltip('enchantments') && (
|
|
<EnchantmentsTooltip data={item.get('enchantments', tag => tag)} />
|
|
)}
|
|
{item.showInTooltip('dyed_color') && (advanced
|
|
? <TextComponent component={{ translate: 'item.color', with: [`#${item.get('dyed_color', tag => tag.isCompound() ? tag.getNumber('rgb') : tag.getAsNumber())?.toString(16).padStart(6, '0')}`], color: 'gray' }} />
|
|
: <TextComponent component={{ translate: 'item.dyed', color: 'gray' }} />
|
|
)}
|
|
{item.getLore().map((component) =>
|
|
<TextComponent component={component} base={{ color: 'dark_purple', italic: true }} />
|
|
)}
|
|
{item.showInTooltip('attribute_modifiers') && (
|
|
<AttributeModifiersTooltip data={item.get('attribute_modifiers', tag => tag)} />
|
|
)}
|
|
{item.showInTooltip('unbreakable') && (
|
|
<TextComponent component={{ translate: 'item.unbreakable', color: 'blue' }} />
|
|
)}
|
|
{item.has('ominous_bottle_amplifier') && (
|
|
<PotionContentsTooltip contents={{ customEffects: [{ effect: Identifier.create('bad_omen'), amplifier: item.get('ominous_bottle_amplifier', tag => tag.getAsNumber()) ?? 0, duration: 120000 }]}} />
|
|
)}
|
|
{/* TODO: creative-only suspicious stew effects */}
|
|
{/* TODO: can break and can place on */}
|
|
{advanced && item.isDamageable() && (
|
|
<TextComponent component={{ translate: 'item.durability', with: [`${item.getMaxDamage() - item.getDamage()}`, `${item.getMaxDamage()}`] }} />
|
|
)}
|
|
{advanced && <>
|
|
<TextComponent component={{ text: item.id.toString(), color: 'dark_gray'}} />
|
|
{item.getSize() > 0 && <TextComponent component={{ translate: 'item.components', with: [item.getSize()], color: 'dark_gray' }} />}
|
|
</>}
|
|
</>
|
|
}
|
|
|
|
const BUILTIN_TRIM_MATERIALS: Record<string, string | undefined> = {
|
|
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 <TextComponent component={{ ...component, color }} />
|
|
})}
|
|
{effects.length === 0 && <TextComponent component={{ translate: 'effect.none', color: 'gray' }} />}
|
|
</>
|
|
|
|
}
|
|
|
|
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 <TextComponent component={{ translate: makeDescriptionId('enchantment', id), color: id.path.endsWith('_curse') ? 'red' : 'gray', extra: level === 1 ? [] : [' ', { translate: `enchantment.level.${level}` }] }} />
|
|
})}
|
|
</>
|
|
}
|
|
|
|
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 && <>
|
|
<TextComponent component={''} />
|
|
<TextComponent component={{ translate: `item.modifiers.${group}`, color: 'gray' }} />
|
|
</>}
|
|
{absolute ? (
|
|
<TextComponent component={[' ', { translate: `attribute.modifier.equals.${operation}`, with: [+amount.toFixed(2), { translate: `attribute.name.${type.path}`}], color: 'dark_green' }]} />
|
|
) : amount > 0 ? (
|
|
<TextComponent component={{ translate: `attribute.modifier.plus.${operation}`, with: [+amount.toFixed(2), { translate: `attribute.name.${type.path}`}], color: NEGATIVE_ATTRIBUTES.has(type.toString()) ? 'red' : NEUTRAL_ATTRIBUTES.has(type.toString()) ? 'gray' : 'blue' }} />
|
|
) : amount < 0 ? (
|
|
<TextComponent component={{ translate: `attribute.modifier.take.${operation}`, with: [+(-amount).toFixed(2), { translate: `attribute.name.${type.path}`}], color: NEGATIVE_ATTRIBUTES.has(type.toString()) ? 'blue' : NEUTRAL_ATTRIBUTES.has(type.toString()) ? 'gray' : 'red'}} />
|
|
) : <></>}
|
|
</>
|
|
})
|
|
})}
|
|
</>
|
|
}
|