diff --git a/index.js b/index.js index e91fb33..d214f62 100644 --- a/index.js +++ b/index.js @@ -95,6 +95,52 @@ function provider (registry, { Biome, version }) { return 0 } + // Block tags (registry.tags, minecraft-data 1.13+) describe which tools mine a block since 1.17. When present they + // replace the single-material speed lookup: a block can be in several mineable/* tags, and the sword and shears + // rules below are the vanilla ToolMaterial/ShearsItem constants, which are not in any data file. + const blockTags = registry.tags?.['minecraft:block'] + const tagSets = {} + function inTag (tag, blockName) { + if (!tagSets[tag]) tagSets[tag] = new Set(blockTags?.[`minecraft:${tag}`] ?? []) + return tagSets[tag].has(`minecraft:${blockName}`) + } + const hasToolTags = Boolean(blockTags?.['minecraft:mineable/pickaxe']) + const TOOL_TYPES = ['pickaxe', 'axe', 'shovel', 'hoe'] + + function tagBreakingSpeed (block, heldItemType) { + const item = registry.items[heldItemType] + if (!item) return 1 + const name = block.name + if (item.name === 'shears') { + if (name === 'cobweb' || inTag('leaves', name)) return 15 + if (inTag('wool', name)) return 5 + if (name === 'vine' || name === 'glow_lichen') return 2 + return 1 + } + if (item.name.endsWith('_sword')) { + if (name === 'cobweb') return 15 + // bamboo breaks instantly with a sword: BambooStalkBlock/BambooSaplingBlock.getDestroyProgress up to 1.21.4, the + // sword_instantly_mines tag from 1.21.5 + const instantly = blockTags['minecraft:sword_instantly_mines'] ? inTag('sword_instantly_mines', name) : (name === 'bamboo' || name === 'bamboo_sapling') + if (instantly) return Infinity + // 1.20+: SwordItem.getDestroySpeed is 1.5 for the sword_efficient tag. + if (blockTags['minecraft:sword_efficient']) return inTag('sword_efficient', name) ? 1.5 : 1 + // 1.17 to 1.19 have no sword_efficient tag: getDestroySpeed returns 1.5 for the leaves tag and the PLANT, + // REPLACEABLE_PLANT, VEGETABLE (gourd) and VINE materials, 1 otherwise. These are ToolMaterial constants, so + // encode them here rather than trusting the material table, whose composite sword speeds do not survive + // regeneration (e.g. leaves' sword speed drops from 1.5 to 1 when the tags are rebuilt). + if (inTag('leaves', name)) return 1.5 + const materials = (block.material ?? '').split(';') + if (materials.some(m => m === 'plant' || m === 'gourd' || m === 'vine_or_glow_lichen')) return 1.5 + return 1 + } + for (const type of TOOL_TYPES) { + const speed = registry.materials[`mineable/${type}`]?.[heldItemType] + if (speed && inTag(`mineable/${type}`, name)) return speed + } + return 1 + } + function getMiningFatigueMultiplier (effectLevel) { switch (effectLevel) { case 0: return 1.0 @@ -306,14 +352,17 @@ function provider (registry, { Biome, version }) { digTime (heldItemType, creative, inWater, notOnGround, enchantments = [], effects = {}) { if (creative) return 0 - const materialToolMultipliers = registry.materials[this.material] - const isBestTool = heldItemType && materialToolMultipliers && materialToolMultipliers[heldItemType] - // Compute breaking speed multiplier let blockBreakingSpeed = 1 - if (isBestTool) { - blockBreakingSpeed = materialToolMultipliers[heldItemType] + if (heldItemType && hasToolTags) { + blockBreakingSpeed = tagBreakingSpeed(this, heldItemType) + if (blockBreakingSpeed === Infinity) return 0 + } else { + const materialToolMultipliers = registry.materials[this.material] + if (heldItemType && materialToolMultipliers && materialToolMultipliers[heldItemType]) { + blockBreakingSpeed = materialToolMultipliers[heldItemType] + } } // Efficiency is applied if tools speed multiplier is more than 1.0 diff --git a/test/basic.test.js b/test/basic.test.js index 8c16554..13d805a 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -2,6 +2,92 @@ const expect = require('expect').default +// Block tags decide which tools mine a block from 1.17 on (minecraft-data tags.json, exposed as registry.tags). +// The fixture is the subset of vanilla 26.1 tags these cases need, in the tags.json shape. +describe('Dig time from block tags', () => { + const registry = require('prismarine-registry')('26.1') + registry.tags = { + 'minecraft:block': { + 'minecraft:leaves': ['minecraft:oak_leaves'], + 'minecraft:mineable/axe': ['minecraft:melon', 'minecraft:oak_log'], + 'minecraft:mineable/hoe': ['minecraft:oak_leaves'], + 'minecraft:mineable/pickaxe': ['minecraft:iron_ore', 'minecraft:obsidian', 'minecraft:stone'], + 'minecraft:sword_efficient': ['minecraft:melon'], + 'minecraft:sword_instantly_mines': ['minecraft:bamboo'], + 'minecraft:wool': ['minecraft:white_wool'] + } + } + const Block = require('prismarine-block')(registry) + const block = name => Block.fromStateId(registry.blocksByName[name].defaultState, 0) + const item = name => registry.itemsByName[name].id + const dig = (blockName, itemName, enchantments = []) => block(blockName).digTime(itemName ? item(itemName) : null, false, false, false, enchantments) + + it('uses the pickaxe tier speed for a tier-gated ore', () => { + expect(dig('iron_ore', 'diamond_pickaxe')).toBe(600) + expect(dig('obsidian', 'diamond_pickaxe')).toBe(9400) + }) + it('applies efficiency on top of the tag speed', () => { + expect(dig('iron_ore', 'diamond_pickaxe', [{ name: 'efficiency', lvl: 5 }])).toBe(150) + }) + it('keeps the harvest penalty for a tool below the required tier', () => { + expect(dig('iron_ore', 'wooden_pickaxe')).toBe(7500) + expect(dig('stone', null)).toBe(7500) + }) + it('uses the vanilla sword rules', () => { + expect(dig('melon', 'diamond_sword')).toBe(1000) + expect(dig('melon', 'diamond_axe')).toBe(200) + expect(dig('bamboo', 'diamond_sword')).toBe(0) + }) + it('uses the vanilla shears rules', () => { + expect(dig('oak_leaves', 'shears')).toBe(0) + expect(dig('white_wool', 'shears')).toBe(250) + }) + it('ignores tools that do not apply to the block', () => { + expect(dig('oak_log', 'diamond_pickaxe')).toBe(dig('oak_log', null)) + }) +}) + +describe('Dig time from block tags before the sword tags (1.17 to 1.19)', () => { + const registry = require('prismarine-registry')('1.19.4') + registry.tags = { + 'minecraft:block': { + 'minecraft:leaves': ['minecraft:oak_leaves'], + 'minecraft:mineable/axe': ['minecraft:bamboo', 'minecraft:melon'], + 'minecraft:mineable/hoe': ['minecraft:oak_leaves'], + 'minecraft:mineable/pickaxe': ['minecraft:stone'] + } + } + const Block = require('prismarine-block')(registry) + const block = name => Block.fromStateId(registry.blocksByName[name].defaultState, 0) + const dig = (blockName, itemName) => block(blockName).digTime(registry.itemsByName[itemName].id, false, false, false) + + it('applies the pre-1.20 sword rule from the leaves tag and plant materials', () => { + // SwordItem.getDestroySpeed is 1.5 for the leaves tag and the PLANT/VEGETABLE/VINE materials. This comes from the + // tag and material names, not the material table's composite sword speeds, which drop to 1 when tags are regenerated. + expect(dig('oak_leaves', 'diamond_sword')).toBe(200) // leaves tag -> 1.5 + expect(dig('melon', 'diamond_sword')).toBe(1000) // gourd (VEGETABLE) material -> 1.5 + expect(dig('vine', 'diamond_sword')).toBe(200) // vine material -> 1.5 + expect(dig('stone', 'diamond_sword')).toBe(7500) // no sword rule -> 1 + }) + it('mines bamboo instantly with a sword', () => { + expect(dig('bamboo', 'diamond_sword')).toBe(0) + expect(dig('bamboo', 'diamond_axe')).toBe(200) + }) + it('still takes the tool tiers from the mineable tags', () => { + expect(dig('oak_leaves', 'diamond_hoe')).toBe(0) + expect(dig('stone', 'diamond_pickaxe')).toBe(300) + }) +}) + +describe('Dig time without block tags (before 1.17)', () => { + const registry = require('prismarine-registry')('1.16.4') + const Block = require('prismarine-block')(registry) + it('mines stone with an iron pickaxe from materials', () => { + const stone = Block.fromStateId(registry.blocksByName.stone.defaultState, 0) + expect(stone.digTime(registry.itemsByName.iron_pickaxe.id, false, false, false)).toBe(400) + }) +}) + // https://minecraft.gamepedia.com/Breaking#Blocks_by_hardness describe('Dig time', () => { describe('1.20.5', () => {