mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
1006 lines
31 KiB
TypeScript
1006 lines
31 KiB
TypeScript
import type { Random } from 'deepslate'
|
|
import { LegacyRandom } from 'deepslate'
|
|
import type { VersionId } from '../services/Schemas.js'
|
|
import { clamp, deepClone, getWeightedRandom, isObject } from '../Utils.js'
|
|
|
|
export interface Item {
|
|
id: string,
|
|
count: number,
|
|
tag?: any,
|
|
}
|
|
|
|
export interface SlottedItem {
|
|
slot: number,
|
|
item: Item,
|
|
}
|
|
|
|
type ItemConsumer = (item: Item) => void
|
|
|
|
const StackMixers = {
|
|
container: fillContainer,
|
|
default: assignSlots,
|
|
}
|
|
|
|
type StackMixer = keyof typeof StackMixers
|
|
|
|
interface LootOptions {
|
|
version: VersionId,
|
|
seed: bigint,
|
|
luck: number,
|
|
daytime: number,
|
|
weather: string,
|
|
stackMixer: StackMixer,
|
|
}
|
|
|
|
interface LootContext extends LootOptions {
|
|
random: Random,
|
|
luck: number
|
|
weather: string,
|
|
dayTime: number,
|
|
getItemTag(id: string): string[],
|
|
getLootTable(id: string): any,
|
|
getPredicate(id: string): any,
|
|
}
|
|
|
|
export function generateLootTable(lootTable: any, options: LootOptions) {
|
|
const ctx = createLootContext(options)
|
|
const result: Item[] = []
|
|
generateTable(lootTable, item => result.push(item), ctx)
|
|
const mixer = StackMixers[options.stackMixer]
|
|
return mixer(result, ctx)
|
|
}
|
|
|
|
const SLOT_COUNT = 27
|
|
|
|
function fillContainer(items: Item[], ctx: LootContext): SlottedItem[] {
|
|
const slots = shuffle([...Array(SLOT_COUNT)].map((_, i) => i), ctx)
|
|
|
|
const queue = items.filter(i => i.id !== 'minecraft:air' && i.count > 1)
|
|
items = items.filter(i => i.id !== 'minecraft:air' && i.count === 1)
|
|
|
|
while (SLOT_COUNT - items.length - queue.length > 0 && queue.length > 0) {
|
|
const [itemA] = queue.splice(ctx.random.nextInt(queue.length), 1)
|
|
const splitCount = ctx.random.nextInt(Math.floor(itemA.count / 2)) + 1
|
|
const itemB = splitItem(itemA, splitCount)
|
|
|
|
for (const item of [itemA, itemB]) {
|
|
if (item.count > 1 && ctx.random.nextFloat() < 0.5) {
|
|
queue.push(item)
|
|
} else {
|
|
items.push(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
items.push(...queue)
|
|
shuffle(items, ctx)
|
|
|
|
const results: SlottedItem[] = []
|
|
for (const item of items) {
|
|
const slot = slots.pop()
|
|
if (slot === undefined) {
|
|
break
|
|
}
|
|
if (item.id !== 'minecraft:air' && item.count > 0) {
|
|
results.push({ slot, item })
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
function assignSlots(items: Item[]): SlottedItem[] {
|
|
return items.map((item, i) => ({ slot: i, item }))
|
|
}
|
|
|
|
function splitItem(item: Item, count: number): Item {
|
|
const splitCount = Math.min(count, item.count)
|
|
const other = deepClone(item)
|
|
other.count = splitCount
|
|
item.count = item.count - splitCount
|
|
return other
|
|
}
|
|
|
|
function shuffle<T>(array: T[], ctx: LootContext) {
|
|
let i = array.length
|
|
while (i > 0) {
|
|
const j = ctx.random.nextInt(i)
|
|
i -= 1;
|
|
[array[i], array[j]] = [array[j], array[i]]
|
|
}
|
|
return array
|
|
}
|
|
|
|
function generateTable(table: any, consumer: ItemConsumer, ctx: LootContext) {
|
|
const tableConsumer = decorateFunctions(table.functions ?? [], consumer, ctx)
|
|
for (const pool of table.pools ?? []) {
|
|
generatePool(pool, tableConsumer, ctx)
|
|
}
|
|
}
|
|
|
|
function createLootContext(options: LootOptions): LootContext {
|
|
return {
|
|
...options,
|
|
random: new LegacyRandom(options.seed),
|
|
luck: options.luck,
|
|
weather: options.weather,
|
|
dayTime: options.daytime,
|
|
getItemTag: () => [],
|
|
getLootTable: () => ({ pools: [] }),
|
|
getPredicate: () => [],
|
|
}
|
|
}
|
|
|
|
function generatePool(pool: any, consumer: ItemConsumer, ctx: LootContext) {
|
|
if (composeConditions(pool.conditions ?? [])(ctx)) {
|
|
const poolConsumer = decorateFunctions(pool.functions ?? [], consumer, ctx)
|
|
|
|
const rolls = computeInt(pool.rolls, ctx) + Math.floor(computeFloat(pool.bonus_rolls, ctx) * ctx.luck)
|
|
for (let i = 0; i < rolls; i += 1) {
|
|
let totalWeight = 0
|
|
const entries: any[] = []
|
|
|
|
// Expand entries
|
|
for (const entry of pool.entries ?? []) {
|
|
expandEntry(entry, ctx, (e) => {
|
|
const weight = computeWeight(e, ctx.luck)
|
|
if (weight > 0) {
|
|
entries.push(e)
|
|
totalWeight += weight
|
|
}
|
|
})
|
|
}
|
|
|
|
// Select random entry
|
|
if (totalWeight === 0 || entries.length === 0) {
|
|
continue
|
|
}
|
|
if (entries.length === 1) {
|
|
createItem(entries[0], poolConsumer, ctx)
|
|
continue
|
|
}
|
|
let remainingWeight = ctx.random.nextInt(totalWeight)
|
|
for (const entry of entries) {
|
|
remainingWeight -= computeWeight(entry, ctx.luck)
|
|
if (remainingWeight < 0) {
|
|
createItem(entry, poolConsumer, ctx)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function expandEntry(entry: any, ctx: LootContext, consumer: (entry: any) => void): boolean {
|
|
if (!canEntryRun(entry, ctx)) {
|
|
return false
|
|
}
|
|
const type = entry.type?.replace(/^minecraft:/, '')
|
|
switch (type) {
|
|
case 'group':
|
|
for (const child of entry.children ?? []) {
|
|
expandEntry(child, ctx, consumer)
|
|
}
|
|
return true
|
|
case 'alternatives':
|
|
for (const child of entry.children ?? []) {
|
|
if (expandEntry(child, ctx, consumer)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
case 'sequence':
|
|
for (const child of entry.children ?? []) {
|
|
if (!expandEntry(child, ctx, consumer)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case 'tag':
|
|
if (entry.expand) {
|
|
ctx.getItemTag(entry.tag ?? '').forEach(tagEntry => {
|
|
consumer({ type: 'item', name: tagEntry })
|
|
})
|
|
} else {
|
|
consumer(entry)
|
|
}
|
|
return true
|
|
default:
|
|
consumer(entry)
|
|
return true
|
|
}
|
|
}
|
|
|
|
function canEntryRun(entry: any, ctx: LootContext): boolean {
|
|
return composeConditions(entry.conditions ?? [])(ctx)
|
|
}
|
|
|
|
function createItem(entry: any, consumer: ItemConsumer, ctx: LootContext) {
|
|
const entryConsumer = decorateFunctions(entry.functions ?? [], consumer, ctx)
|
|
|
|
const type = entry.type?.replace(/^minecraft:/, '')
|
|
switch (type) {
|
|
case 'item':
|
|
entryConsumer({ id: entry.name, count: 1 })
|
|
break
|
|
case 'tag':
|
|
ctx.getItemTag(entry.name ?? '').forEach(tagEntry => {
|
|
entryConsumer({ id: tagEntry, count: 1 })
|
|
})
|
|
break
|
|
case 'loot_table':
|
|
generateTable(ctx.getLootTable(entry.name), entryConsumer, ctx)
|
|
break
|
|
case 'dynamic':
|
|
// not relevant for this simulation
|
|
break
|
|
}
|
|
}
|
|
|
|
function computeWeight(entry: any, luck: number) {
|
|
return Math.max(Math.floor((entry.weight ?? 1) + (entry.quality ?? 0) * luck), 0)
|
|
}
|
|
|
|
type LootFunction = (item: Item, ctx: LootContext) => void
|
|
|
|
function decorateFunctions(functions: any[], consumer: ItemConsumer, ctx: LootContext): ItemConsumer {
|
|
const compositeFunction = composeFunctions(functions)
|
|
return (item) => {
|
|
compositeFunction(item, ctx)
|
|
consumer(item)
|
|
}
|
|
}
|
|
|
|
function composeFunctions(functions: any[]): LootFunction {
|
|
return (item, ctx) => {
|
|
for (const fn of functions) {
|
|
if (composeConditions(fn.conditions ?? [])(ctx)) {
|
|
const type = fn.function?.replace(/^minecraft:/, '');
|
|
(LootFunctions[type]?.(fn) ?? (i => i))(item, ctx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const LootFunctions: Record<string, (params: any) => LootFunction> = {
|
|
enchant_randomly: ({ enchantments }) => (item, ctx) => {
|
|
const isBook = item.id === 'minecraft:book'
|
|
if (enchantments === undefined || enchantments.length === 0) {
|
|
enchantments = [...Enchantments.keys()]
|
|
.filter(e => {
|
|
const data = getEnchantmentData(e)
|
|
return data.discoverable && (isBook || data.canEnchant(item.id))
|
|
})
|
|
}
|
|
if (enchantments.length > 0) {
|
|
const id = enchantments[ctx.random.nextInt(enchantments.length)]
|
|
const data = getEnchantmentData(id)
|
|
const lvl = ctx.random.nextInt(data.maxLevel - data.minLevel + 1) + data.minLevel
|
|
if (isBook) {
|
|
item.tag = {}
|
|
item.count = 1
|
|
}
|
|
enchantItem(item, { id, lvl })
|
|
if (isBook) {
|
|
item.id = 'minecraft:enchanted_book'
|
|
}
|
|
}
|
|
},
|
|
enchant_with_levels: ({ levels, treasure }) => (item, ctx) => {
|
|
const enchants = selectEnchantments(ctx.random, item, computeInt(levels, ctx), treasure)
|
|
const isBook = item.id === 'minecraft:book'
|
|
if (isBook) {
|
|
item.count = 1
|
|
item.tag = {}
|
|
}
|
|
for (const enchant of enchants) {
|
|
enchantItem(item, enchant)
|
|
}
|
|
if (isBook) {
|
|
item.id = 'minecraft:enchanted_book'
|
|
}
|
|
},
|
|
limit_count: ({ limit }) => (item, ctx) => {
|
|
const { min, max } = prepareIntRange(limit, ctx)
|
|
item.count = clamp(item.count, min, max )
|
|
},
|
|
set_count: ({ count }) => (item, ctx) => {
|
|
item.count = computeInt(count, ctx)
|
|
},
|
|
set_damage: ({ damage, add }) => (item, ctx) => {
|
|
const maxDamage = MaxDamageItems.get(item.id)
|
|
if (maxDamage) {
|
|
const oldDamage = add ? 1 - (item.tag?.Damage ?? 0) / maxDamage : 0
|
|
const newDamage = 1 - clamp(computeFloat(damage, ctx) + oldDamage, 0, 1)
|
|
const finalDamage = Math.floor(newDamage * maxDamage)
|
|
item.tag = { ...item.tag, Damage: finalDamage }
|
|
}
|
|
},
|
|
set_enchantments: ({ enchantments, add }) => (item, ctx) => {
|
|
Object.entries(enchantments).forEach(([id, level]) => {
|
|
const lvl = computeInt(level, ctx)
|
|
enchantItem(item, { id, lvl }, add)
|
|
})
|
|
},
|
|
set_lore: ({ lore, replace }) => (item) => {
|
|
const lines = lore.map((line: any) => JSON.stringify(line))
|
|
const newLore = replace ? lines : [...(item.tag?.display?.Lore ?? []), ...lines]
|
|
item.tag = { ...item.tag, display: { ...item.tag?.display, Lore: newLore } }
|
|
},
|
|
set_name: ({ name }) => (item) => {
|
|
const newName = JSON.stringify(name)
|
|
item.tag = { ...item.tag, display: { ...item.tag?.display, Name: newName } }
|
|
},
|
|
}
|
|
|
|
type LootCondition = (ctx: LootContext) => boolean
|
|
|
|
function composeConditions(conditions: any[]): LootCondition {
|
|
return (ctx) => {
|
|
for (const cond of conditions) {
|
|
if (!testCondition(cond, ctx)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
function testCondition(condition: any, ctx: LootContext): boolean {
|
|
const type = condition.condition?.replace(/^minecraft:/, '')
|
|
return (LootConditions[type]?.(condition) ?? (() => true))(ctx)
|
|
}
|
|
|
|
const LootConditions: Record<string, (params: any) => LootCondition> = {
|
|
alternative: ({ terms }) => (ctx) => {
|
|
for (const term of terms) {
|
|
if (testCondition(term, ctx)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
block_state_property: () => () => {
|
|
return false // TODO
|
|
},
|
|
damage_source_properties: ({ predicate }) => (ctx) => {
|
|
return testDamageSourcePredicate(predicate, ctx)
|
|
},
|
|
entity_properties: ({ predicate }) => (ctx) => {
|
|
return testEntityPredicate(predicate, ctx)
|
|
},
|
|
entity_scores: () => () => {
|
|
return false // TODO,
|
|
},
|
|
inverted: ({ term }) => (ctx) => {
|
|
return !testCondition(term, ctx)
|
|
},
|
|
killed_by_player: ({ inverted }) => () => {
|
|
return (inverted ?? false) === false // TODO
|
|
},
|
|
location_check: ({ predicate }) => (ctx) => {
|
|
return testLocationPredicate(predicate, ctx)
|
|
},
|
|
match_tool: ({ predicate }) => (ctx) => {
|
|
return testItemPredicate(predicate, ctx)
|
|
},
|
|
random_chance: ({ chance }) => (ctx) => {
|
|
return ctx.random.nextFloat() < chance
|
|
},
|
|
random_chance_with_looting: ({ chance, looting_multiplier }) => (ctx) => {
|
|
const level = 0 // TODO: get looting level from killer
|
|
const probability = chance + level * looting_multiplier
|
|
return ctx.random.nextFloat() < probability
|
|
|
|
},
|
|
reference: ({ name }) => (ctx) => {
|
|
const predicate = ctx.getPredicate(name) ?? []
|
|
if (Array.isArray(predicate)) {
|
|
return composeConditions(predicate)(ctx)
|
|
}
|
|
return testCondition(predicate, ctx)
|
|
},
|
|
survives_explosion: () => () => true,
|
|
table_bonus: ({ chances }) => (ctx) => {
|
|
const level = 0 // TODO: get enchantment level from tool
|
|
const chance = chances[clamp(level, 0, chances.length - 1)]
|
|
return ctx.random.nextFloat() < chance
|
|
},
|
|
time_check: ({ value, period }) => (ctx) => {
|
|
let time = ctx.dayTime
|
|
if (period !== undefined) {
|
|
time = time % period
|
|
}
|
|
const { min, max } = prepareIntRange(value, ctx)
|
|
return min <= time && time <= max
|
|
},
|
|
value_check: () => () => {
|
|
return false // TODO
|
|
},
|
|
weather_check: ({ raining, thundering }) => (ctx) => {
|
|
const isRaining = ctx.weather === 'rain' || ctx.weather === 'thunder'
|
|
const isThundering = ctx.weather === 'thunder'
|
|
if (raining !== undefined && raining !== isRaining) return false
|
|
if (thundering !== undefined && thundering !== isThundering) return false
|
|
return true
|
|
},
|
|
}
|
|
|
|
function computeInt(provider: any, ctx: LootContext): number {
|
|
if (typeof provider === 'number') return provider
|
|
if (!isObject(provider)) return 0
|
|
|
|
const type = provider.type?.replace(/^minecraft:/, '') ?? 'uniform'
|
|
switch (type) {
|
|
case 'constant':
|
|
return Math.round(provider.value ?? 0)
|
|
case 'uniform':
|
|
const min = computeInt(provider.min, ctx)
|
|
const max = computeInt(provider.max, ctx)
|
|
return max < min ? min : ctx.random.nextInt(max - min + 1) + min
|
|
case 'binomial':
|
|
const n = computeInt(provider.n, ctx)
|
|
const p = computeFloat(provider.p, ctx)
|
|
let result = 0
|
|
for (let i = 0; i < n; i += 1) {
|
|
if (ctx.random.nextFloat() < p) {
|
|
result += 1
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function computeFloat(provider: any, ctx: LootContext): number {
|
|
if (typeof provider === 'number') return provider
|
|
if (!isObject(provider)) return 0
|
|
|
|
const type = provider.type?.replace(/^minecraft:/, '') ?? 'uniform'
|
|
switch (type) {
|
|
case 'constant':
|
|
return provider.value ?? 0
|
|
case 'uniform':
|
|
const min = computeFloat(provider.min, ctx)
|
|
const max = computeFloat(provider.max, ctx)
|
|
return max < min ? min : ctx.random.nextFloat() * (max-min) + min
|
|
case 'binomial':
|
|
const n = computeInt(provider.n, ctx)
|
|
const p = computeFloat(provider.p, ctx)
|
|
let result = 0
|
|
for (let i = 0; i < n; i += 1) {
|
|
if (ctx.random.nextFloat() < p) {
|
|
result += 1
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function prepareIntRange(range: any, ctx: LootContext) {
|
|
if (typeof range === 'number') {
|
|
range = { min: range, max: range }
|
|
}
|
|
const min = computeInt(range.min, ctx)
|
|
const max = computeInt(range.max, ctx)
|
|
return { min, max }
|
|
}
|
|
|
|
function testItemPredicate(_predicate: any, _ctx: LootContext) {
|
|
return false // TODO
|
|
}
|
|
|
|
function testLocationPredicate(_predicate: any, _ctx: LootContext) {
|
|
return false // TODO
|
|
}
|
|
|
|
function testEntityPredicate(_predicate: any, _ctx: LootContext) {
|
|
return false // TODO
|
|
}
|
|
|
|
function testDamageSourcePredicate(_predicate: any, _ctx: LootContext) {
|
|
return false // TODO
|
|
}
|
|
|
|
function enchantItem(item: Item, enchant: Enchant, additive?: boolean) {
|
|
if (!item.tag) {
|
|
item.tag = {}
|
|
}
|
|
const listKey = (item.id === 'minecraft:book') ? 'StoredEnchantments' : 'Enchantments'
|
|
if (!item.tag[listKey] || !Array.isArray(item.tag[listKey])) {
|
|
item.tag[listKey] = []
|
|
}
|
|
const enchantments = item.tag[listKey] as any[]
|
|
let index = enchantments.findIndex((e: any) => e.id === enchant.id)
|
|
if (index !== -1) {
|
|
const oldEnch = enchantments[index]
|
|
oldEnch.lvl = Math.max(additive ? oldEnch.lvl + enchant.lvl : enchant.lvl, 0)
|
|
} else {
|
|
enchantments.push(enchant)
|
|
index = enchantments.length - 1
|
|
}
|
|
if (enchantments[index].lvl === 0) {
|
|
enchantments.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
function selectEnchantments(random: Random, item: Item, levels: number, treasure: boolean): Enchant[] {
|
|
const enchantmentValue = EnchantmentItems.get(item.id) ?? 0
|
|
if (enchantmentValue <= 0) {
|
|
return []
|
|
}
|
|
levels += 1 + random.nextInt(Math.floor(enchantmentValue / 4 + 1)) + random.nextInt(Math.floor(enchantmentValue / 4 + 1))
|
|
const f = (random.nextFloat() + random.nextFloat() - 1) * 0.15
|
|
levels = clamp(Math.round(levels + levels * f), 1, Number.MAX_SAFE_INTEGER)
|
|
let available = getAvailableEnchantments(item, levels, treasure)
|
|
if (available.length === 0) {
|
|
return []
|
|
}
|
|
const result = []
|
|
const first = getWeightedRandom(random, available, getEnchantWeight)
|
|
if (first) result.push(first)
|
|
|
|
while (random.nextInt(50) <= levels) {
|
|
if (result.length > 0) {
|
|
const lastAdded = result[result.length - 1]
|
|
available = available.filter(a => isEnchantCompatible(a.id, lastAdded.id))
|
|
}
|
|
if (available.length === 0) break
|
|
const ench = getWeightedRandom(random, available, getEnchantWeight)
|
|
if (ench) result.push(ench)
|
|
levels = Math.floor(levels / 2)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
function getEnchantWeight(ench: Enchant) {
|
|
return EnchantmentsRarityWeights.get(getEnchantmentData(ench.id)?.rarity ?? 'common') ?? 0
|
|
}
|
|
|
|
function getAvailableEnchantments(item: Item, levels: number, treasure: boolean): Enchant[] {
|
|
const result = []
|
|
const isBook = item.id === 'minecraft:book'
|
|
|
|
for (const id of Enchantments.keys()) {
|
|
const ench = getEnchantmentData(id)!
|
|
if ((!ench.treasure || treasure) && ench.discoverable && (ench.canEnchant(item.id) || isBook)) {
|
|
for (let lvl = ench.maxLevel; lvl > ench.minLevel - 1; lvl -= 1) {
|
|
if (levels >= ench.minCost(lvl) && levels <= ench.maxCost(lvl)) {
|
|
result.push({ id, lvl })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
interface Enchant {
|
|
id: string,
|
|
lvl: number,
|
|
}
|
|
|
|
function isEnchantCompatible(a: string, b: string) {
|
|
return a !== b && isEnchantCompatibleRaw(a, b) && isEnchantCompatibleRaw(b, a)
|
|
}
|
|
|
|
function isEnchantCompatibleRaw(a: string, b: string) {
|
|
const ench = getEnchantmentData(a)
|
|
return ench?.isCompatible(b)
|
|
}
|
|
|
|
export const MaxDamageItems = new Map(Object.entries<number>({
|
|
'minecraft:carrot_on_a_stick': 25,
|
|
'minecraft:warped_fungus_on_a_stick': 100,
|
|
'minecraft:flint_and_steel': 64,
|
|
'minecraft:elytra': 432,
|
|
'minecraft:bow': 384,
|
|
'minecraft:fishing_rod': 64,
|
|
'minecraft:shears': 238,
|
|
'minecraft:shield': 336,
|
|
'minecraft:trident': 250,
|
|
'minecraft:crossbow': 465,
|
|
|
|
'minecraft:leather_helmet': 11 * 5,
|
|
'minecraft:leather_chestplate': 16 * 5,
|
|
'minecraft:leather_leggings': 15 * 5,
|
|
'minecraft:leather_boots': 13 * 5,
|
|
'minecraft:chainmail_helmet': 11 * 15,
|
|
'minecraft:chainmail_chestplate': 16 * 15,
|
|
'minecraft:chainmail_leggings': 15 * 15,
|
|
'minecraft:chainmail_boots': 13 * 15,
|
|
'minecraft:iron_helmet': 11 * 15,
|
|
'minecraft:iron_chestplate': 16 * 15,
|
|
'minecraft:iron_leggings': 15 * 15,
|
|
'minecraft:iron_boots': 13 * 15,
|
|
'minecraft:diamond_helmet': 11 * 33,
|
|
'minecraft:diamond_chestplate': 16 * 33,
|
|
'minecraft:diamond_leggings': 15 * 33,
|
|
'minecraft:diamond_boots': 13 * 33,
|
|
'minecraft:golden_helmet': 11 * 7,
|
|
'minecraft:golden_chestplate': 16 * 7,
|
|
'minecraft:golden_leggings': 15 * 7,
|
|
'minecraft:golden_boots': 13 * 7,
|
|
'minecraft:netherite_helmet': 11 * 37,
|
|
'minecraft:netherite_chestplate': 16 * 37,
|
|
'minecraft:netherite_leggings': 15 * 37,
|
|
'minecraft:netherite_boots': 13 * 37,
|
|
'minecraft:turtle_helmet': 11 * 25,
|
|
|
|
'minecraft:wooden_sword': 59,
|
|
'minecraft:wooden_shovel': 59,
|
|
'minecraft:wooden_pickaxe': 59,
|
|
'minecraft:wooden_axe': 59,
|
|
'minecraft:wooden_hoe': 59,
|
|
'minecraft:stone_sword': 131,
|
|
'minecraft:stone_shovel': 131,
|
|
'minecraft:stone_pickaxe': 131,
|
|
'minecraft:stone_axe': 131,
|
|
'minecraft:stone_hoe': 131,
|
|
'minecraft:iron_sword': 250,
|
|
'minecraft:iron_shovel': 250,
|
|
'minecraft:iron_pickaxe': 250,
|
|
'minecraft:iron_axe': 250,
|
|
'minecraft:iron_hoe': 250,
|
|
'minecraft:diamond_sword': 1561,
|
|
'minecraft:diamond_shovel': 1561,
|
|
'minecraft:diamond_pickaxe': 1561,
|
|
'minecraft:diamond_axe': 1561,
|
|
'minecraft:diamond_hoe': 1561,
|
|
'minecraft:gold_sword': 32,
|
|
'minecraft:gold_shovel': 32,
|
|
'minecraft:gold_pickaxe': 32,
|
|
'minecraft:gold_axe': 32,
|
|
'minecraft:gold_hoe': 32,
|
|
'minecraft:netherite_sword': 2031,
|
|
'minecraft:netherite_shovel': 2031,
|
|
'minecraft:netherite_pickaxe': 2031,
|
|
'minecraft:netherite_axe': 2031,
|
|
'minecraft:netherite_hoe': 2031,
|
|
}))
|
|
|
|
const EnchantmentItems = new Map(Object.entries<number>({
|
|
'minecraft:book': 1,
|
|
'minecraft:fishing_rod': 1,
|
|
'minecraft:trident': 1,
|
|
'minecraft:bow': 1,
|
|
'minecraft:crossbow': 1,
|
|
|
|
'minecraft:leather_helmet': 15,
|
|
'minecraft:leather_chestplate': 15,
|
|
'minecraft:leather_leggings': 15,
|
|
'minecraft:leather_boots': 15,
|
|
'minecraft:chainmail_helmet': 12,
|
|
'minecraft:chainmail_chestplate': 12,
|
|
'minecraft:chainmail_leggings': 12,
|
|
'minecraft:chainmail_boots': 12,
|
|
'minecraft:iron_helmet': 9,
|
|
'minecraft:iron_chestplate': 9,
|
|
'minecraft:iron_leggings': 9,
|
|
'minecraft:iron_boots': 9,
|
|
'minecraft:diamond_helmet': 10,
|
|
'minecraft:diamond_chestplate': 10,
|
|
'minecraft:diamond_leggings': 10,
|
|
'minecraft:diamond_boots': 10,
|
|
'minecraft:golden_helmet': 25,
|
|
'minecraft:golden_chestplate': 25,
|
|
'minecraft:golden_leggings': 25,
|
|
'minecraft:golden_boots': 25,
|
|
'minecraft:netherite_helmet': 15,
|
|
'minecraft:netherite_chestplate': 15,
|
|
'minecraft:netherite_leggings': 15,
|
|
'minecraft:netherite_boots': 15,
|
|
'minecraft:turtle_helmet': 15,
|
|
|
|
'minecraft:wooden_sword': 15,
|
|
'minecraft:wooden_shovel': 15,
|
|
'minecraft:wooden_pickaxe': 15,
|
|
'minecraft:wooden_axe': 15,
|
|
'minecraft:wooden_hoe': 15,
|
|
'minecraft:stone_sword': 5,
|
|
'minecraft:stone_shovel': 5,
|
|
'minecraft:stone_pickaxe': 5,
|
|
'minecraft:stone_axe': 5,
|
|
'minecraft:stone_hoe': 5,
|
|
'minecraft:iron_sword': 14,
|
|
'minecraft:iron_shovel': 14,
|
|
'minecraft:iron_pickaxe': 14,
|
|
'minecraft:iron_axe': 14,
|
|
'minecraft:iron_hoe': 14,
|
|
'minecraft:diamond_sword': 10,
|
|
'minecraft:diamond_shovel': 10,
|
|
'minecraft:diamond_pickaxe': 10,
|
|
'minecraft:diamond_axe': 10,
|
|
'minecraft:diamond_hoe': 10,
|
|
'minecraft:gold_sword': 22,
|
|
'minecraft:gold_shovel': 22,
|
|
'minecraft:gold_pickaxe': 22,
|
|
'minecraft:gold_axe': 22,
|
|
'minecraft:gold_hoe': 22,
|
|
'minecraft:netherite_sword': 15,
|
|
'minecraft:netherite_shovel': 15,
|
|
'minecraft:netherite_pickaxe': 15,
|
|
'minecraft:netherite_axe': 15,
|
|
'minecraft:netherite_hoe': 15,
|
|
}))
|
|
|
|
interface EnchantmentData {
|
|
id: string
|
|
rarity: 'common' | 'uncommon' | 'rare' | 'very_rare'
|
|
category: 'armor' | 'armor_feet' | 'armor_legs' | 'armor_chest' | 'armor_head' | 'weapon' | 'digger' | 'fishing_rod' | 'trident' | 'breakable' | 'bow' | 'wearable' | 'crossbow' | 'vanishable'
|
|
minLevel: number
|
|
maxLevel: number
|
|
minCost: (lvl: number) => number
|
|
maxCost: (lvl: number) => number
|
|
discoverable: boolean
|
|
treasure: boolean
|
|
curse: boolean
|
|
canEnchant: (id: string) => boolean
|
|
isCompatible: (other: string) => boolean
|
|
}
|
|
|
|
export function getEnchantmentData(id: string): EnchantmentData {
|
|
const data = Enchantments.get(id)
|
|
const category = data?.category ?? 'armor'
|
|
return {
|
|
id,
|
|
rarity: data?.rarity ?? 'common',
|
|
category,
|
|
minLevel: data?.minLevel ?? 1,
|
|
maxLevel: data?.maxLevel ?? 1,
|
|
minCost: data?.minCost ?? ((lvl) => 1 + lvl * 10),
|
|
maxCost: data?.maxCost ?? ((lvl) => 6 + lvl * 10),
|
|
discoverable: data?.discoverable ?? true,
|
|
treasure: data?.treasure ?? false,
|
|
curse: data?.curse ?? false,
|
|
canEnchant: id => EnchantmentsCategories.get(category)!.includes(id),
|
|
isCompatible: data?.isCompatible ?? (() => true),
|
|
}
|
|
}
|
|
|
|
const PROTECTION_ENCHANTS = ['minecraft:protection', 'minecraft:fire_protection', 'minecraft:blast_protection', 'minecraft:projectile_protection']
|
|
const DAMAGE_ENCHANTS = ['minecraft:sharpness', 'minecraft:smite', 'minecraft:bane_of_arthropods']
|
|
|
|
const Enchantments = new Map(Object.entries<Partial<EnchantmentData>>({
|
|
'minecraft:protection': { rarity: 'common', category: 'armor', maxLevel: 4,
|
|
minCost: lvl => 1 + (lvl - 1) * 11,
|
|
maxCost: lvl => 1 + (lvl - 1) * 11 + 11,
|
|
isCompatible: other => !PROTECTION_ENCHANTS.includes(other) },
|
|
'minecraft:fire_protection': { rarity: 'uncommon', category: 'armor', maxLevel: 4,
|
|
minCost: lvl => 10 + (lvl - 1) * 8,
|
|
maxCost: lvl => 10 + (lvl - 1) * 8 + 8,
|
|
isCompatible: other => !PROTECTION_ENCHANTS.includes(other) },
|
|
'minecraft:feather_falling': { rarity: 'uncommon', category: 'armor_feet', maxLevel: 4,
|
|
minCost: lvl => 5 + (lvl - 1) * 6,
|
|
maxCost: lvl => 5 + (lvl - 1) * 6 + 6 },
|
|
'minecraft:blast_protection': { rarity: 'rare', category: 'armor', maxLevel: 4,
|
|
minCost: lvl => 5 + (lvl - 1) * 8,
|
|
maxCost: lvl => 5 + (lvl - 1) * 8 + 8,
|
|
isCompatible: other => !PROTECTION_ENCHANTS.includes(other) },
|
|
'minecraft:projectile_protection': { rarity: 'uncommon', category: 'armor', maxLevel: 4,
|
|
minCost: lvl => 3 + (lvl - 1) * 6,
|
|
maxCost: lvl => 3 + (lvl - 1) * 6 + 6,
|
|
isCompatible: other => !PROTECTION_ENCHANTS.includes(other) },
|
|
'minecraft:respiration': { rarity: 'rare', category: 'armor_head', maxLevel: 3,
|
|
minCost: lvl => 10 * lvl,
|
|
maxCost: lvl => 10 * lvl + 30 },
|
|
'minecraft:aqua_affinity': { rarity: 'rare', category: 'armor_head',
|
|
minCost: () => 1,
|
|
maxCost: () => 40 },
|
|
'minecraft:thorns': { rarity: 'very_rare', category: 'armor_chest', maxLevel: 3,
|
|
minCost: lvl => 10 + 20 * (lvl - 1),
|
|
maxCost: lvl => 10 + 20 * (lvl - 1) + 50 },
|
|
'minecraft:depth_strider': { rarity: 'rare', category: 'armor_feet', maxLevel: 3,
|
|
minCost: lvl => 10 * lvl,
|
|
maxCost: lvl => 10 * lvl + 15,
|
|
isCompatible: other => other !== 'minecraft:frost_walker' },
|
|
'minecraft:frost_walker': { rarity: 'rare', category: 'armor_feet', maxLevel: 2, treasure: true,
|
|
minCost: lvl => 10 * lvl,
|
|
maxCost: lvl => 10 * lvl + 15,
|
|
isCompatible: other => other !== 'minecraft:depth_strider' },
|
|
'minecraft:binding_curse': { rarity: 'very_rare', category: 'wearable', treasure: true, curse: true,
|
|
minCost: () => 25,
|
|
maxCost: () => 50 },
|
|
'minecraft:soul_speed': { rarity: 'very_rare', category: 'armor_feet', maxLevel: 3,
|
|
discoverable: false, treasure: true,
|
|
minCost: lvl => 10 * lvl,
|
|
maxCost: lvl => 10 * lvl + 15 },
|
|
'minecraft:swift_sneak': { rarity: 'very_rare', category: 'armor_legs', maxLevel: 3,
|
|
discoverable: false, treasure: true,
|
|
minCost: lvl => 25 * lvl,
|
|
maxCost: lvl => 25 * lvl + 50 },
|
|
'minecraft:sharpness': { rarity: 'common', category: 'weapon', maxLevel: 5,
|
|
minCost: lvl => 1 + (lvl - 1) * 11,
|
|
maxCost: lvl => 1 + (lvl - 1) * 11 + 20,
|
|
isCompatible: other => !DAMAGE_ENCHANTS.includes(other) },
|
|
'minecraft:smite': { rarity: 'common', category: 'weapon', maxLevel: 5,
|
|
minCost: lvl => 5 + (lvl - 1) * 8,
|
|
maxCost: lvl => 5 + (lvl - 1) * 8 + 20,
|
|
isCompatible: other => !DAMAGE_ENCHANTS.includes(other) },
|
|
'minecraft:bane_of_arthropods': { rarity: 'common', category: 'weapon', maxLevel: 5,
|
|
minCost: lvl => 5 + (lvl - 1) * 8,
|
|
maxCost: lvl => 5 + (lvl - 1) * 8 + 20,
|
|
isCompatible: other => !DAMAGE_ENCHANTS.includes(other) },
|
|
'minecraft:knockback': { rarity: 'uncommon', category: 'weapon', maxLevel: 2,
|
|
minCost: lvl => 5 + 20 * (lvl - 1),
|
|
maxCost: lvl => 1 + lvl * 10 + 50 },
|
|
'minecraft:fire_aspect': { rarity: 'rare', category: 'weapon', maxLevel: 2,
|
|
minCost: lvl => 5 + 20 * (lvl - 1),
|
|
maxCost: lvl => 1 + lvl * 10 + 50 },
|
|
'minecraft:looting': { rarity: 'rare', category: 'weapon', maxLevel: 3,
|
|
minCost: lvl => 15 + (lvl - 1) * 9,
|
|
maxCost: lvl => 1 + lvl * 10 + 50,
|
|
isCompatible: other => other !== 'minecraft:silk_touch' },
|
|
'minecraft:sweeping': { rarity: 'rare', category: 'weapon', maxLevel: 3,
|
|
minCost: lvl => 5 + (lvl - 1) * 9,
|
|
maxCost: lvl => 5 + (lvl - 1) * 9 + 15 },
|
|
'minecraft:efficiency': { rarity: 'common', category: 'digger', maxLevel: 5,
|
|
minCost: lvl => 1 + 10 * (lvl - 1),
|
|
maxCost: lvl => 1 + lvl * 10 + 50,
|
|
canEnchant: id => id === 'minecraft:shears' || EnchantmentsCategories.get('digger')!.includes(id) },
|
|
'minecraft:silk_touch': { rarity: 'very_rare', category: 'digger',
|
|
minCost: () => 15,
|
|
maxCost: lvl => 1 + lvl * 10 + 50,
|
|
isCompatible: other => other !== 'minecraft:fortune' },
|
|
'minecraft:unbreaking': { rarity: 'uncommon', category: 'breakable', maxLevel: 3,
|
|
minCost: lvl => 5 + (lvl - 1) * 8,
|
|
maxCost: lvl => 1 + lvl * 10 + 50 },
|
|
'minecraft:fortune': { rarity: 'rare', category: 'digger', maxLevel: 3,
|
|
minCost: lvl => 15 + (lvl - 1) * 9,
|
|
maxCost: lvl => 1 + lvl * 10 + 50,
|
|
isCompatible: other => other !== 'minecraft:silk_touch' },
|
|
'minecraft:power': { rarity: 'common', category: 'bow', maxLevel: 5,
|
|
minCost: lvl => 1 + (lvl - 1) * 10,
|
|
maxCost: lvl => 1 + (lvl - 1) * 10 + 15 },
|
|
'minecraft:punch': { rarity: 'rare', category: 'bow', maxLevel: 2,
|
|
minCost: lvl => 12 + (lvl - 1) * 20,
|
|
maxCost: lvl => 12 + (lvl - 1) * 20 + 25 },
|
|
'minecraft:flame': { rarity: 'rare', category: 'bow',
|
|
minCost: () => 20,
|
|
maxCost: () => 50 },
|
|
'minecraft:infinity': { rarity: 'very_rare', category: 'bow',
|
|
minCost: () => 20,
|
|
maxCost: () => 50,
|
|
isCompatible: other => other !== 'minecraft:mending' },
|
|
'minecraft:luck_of_the_sea': { rarity: 'rare', category: 'fishing_rod', maxLevel: 3,
|
|
minCost: lvl => 15 + (lvl - 1) * 9,
|
|
maxCost: lvl => 1 + lvl * 10 + 50,
|
|
isCompatible: other => other !== 'minecraft:silk_touch' },
|
|
'minecraft:lure': { rarity: 'rare', category: 'fishing_rod', maxLevel: 3,
|
|
minCost: lvl => 15 + (lvl - 1) * 9,
|
|
maxCost: lvl => 1 + lvl * 10 + 50 },
|
|
'minecraft:loyalty': { rarity: 'uncommon', category: 'trident', maxLevel: 3,
|
|
minCost: lvl => 5 + lvl * 7,
|
|
maxCost: () => 50 },
|
|
'minecraft:impaling': { rarity: 'rare', category: 'trident', maxLevel: 5,
|
|
minCost: lvl => 1 + (lvl - 1) * 8,
|
|
maxCost: lvl => 1 + (lvl - 1) * 8 + 20 },
|
|
'minecraft:riptide': { rarity: 'rare', category: 'trident', maxLevel: 3,
|
|
minCost: lvl => 5 + lvl * 7,
|
|
maxCost: () => 50,
|
|
isCompatible: other => !['minecraft:riptide', 'minecraft:channeling'].includes(other) },
|
|
'minecraft:channeling': { rarity: 'very_rare', category: 'trident',
|
|
minCost: () => 25,
|
|
maxCost: () => 50 },
|
|
'minecraft:multishot': { rarity: 'rare', category: 'crossbow',
|
|
minCost: () => 20,
|
|
maxCost: () => 50,
|
|
isCompatible: other => other !== 'minecraft:piercing' },
|
|
'minecraft:quick_charge': { rarity: 'uncommon', category: 'crossbow', maxLevel: 3,
|
|
minCost: lvl => 12 + (lvl - 1) * 20,
|
|
maxCost: () => 50 },
|
|
'minecraft:piercing': { rarity: 'common', category: 'crossbow', maxLevel: 4,
|
|
minCost: lvl => 1 + (lvl - 1) * 10,
|
|
maxCost: () => 50,
|
|
isCompatible: other => other !== 'minecraft:multishot' },
|
|
'minecraft:mending': { rarity: 'rare', category: 'breakable', treasure: true,
|
|
minCost: lvl => lvl * 25,
|
|
maxCost: lvl => lvl * 25 + 50 },
|
|
'minecraft:vanishing_curse': { rarity: 'very_rare', category: 'vanishable', treasure: true, curse: true,
|
|
minCost: () => 25,
|
|
maxCost: () => 50 },
|
|
}))
|
|
|
|
const EnchantmentsRarityWeights = new Map(Object.entries<number>({
|
|
common: 10,
|
|
uncommon: 5,
|
|
rare: 2,
|
|
very_rare: 1,
|
|
}))
|
|
|
|
const ARMOR_FEET = [
|
|
'minecraft:leather_boots',
|
|
'minecraft:chainmail_boots',
|
|
'minecraft:iron_boots',
|
|
'minecraft:diamond_boots',
|
|
'minecraft:golden_boots',
|
|
'minecraft:netherite_boots',
|
|
]
|
|
const ARMOR_LEGS = [
|
|
'minecraft:leather_leggings',
|
|
'minecraft:chainmail_leggings',
|
|
'minecraft:iron_leggings',
|
|
'minecraft:diamond_leggings',
|
|
'minecraft:golden_leggings',
|
|
'minecraft:netherite_leggings',
|
|
]
|
|
const ARMOR_CHEST = [
|
|
'minecraft:leather_chestplate',
|
|
'minecraft:chainmail_chestplate',
|
|
'minecraft:iron_chestplate',
|
|
'minecraft:diamond_chestplate',
|
|
'minecraft:golden_chestplate',
|
|
'minecraft:netherite_chestplate',
|
|
]
|
|
const ARMOR_HEAD = [
|
|
'minecraft:leather_helmet',
|
|
'minecraft:chainmail_helmet',
|
|
'minecraft:iron_helmet',
|
|
'minecraft:diamond_helmet',
|
|
'minecraft:golden_helmet',
|
|
'minecraft:netherite_helmet',
|
|
'minecraft:turtle_helmet',
|
|
]
|
|
const ARMOR = [...ARMOR_FEET, ...ARMOR_LEGS, ...ARMOR_CHEST, ...ARMOR_HEAD]
|
|
const SWORD = [
|
|
'minecraft:wooden_sword',
|
|
'minecraft:stone_sword',
|
|
'minecraft:iron_sword',
|
|
'minecraft:diamond_sword',
|
|
'minecraft:gold_sword',
|
|
'minecraft:netherite_sword',
|
|
]
|
|
const DIGGER = [
|
|
'minecraft:wooden_shovel',
|
|
'minecraft:wooden_pickaxe',
|
|
'minecraft:wooden_axe',
|
|
'minecraft:wooden_hoe',
|
|
'minecraft:stone_shovel',
|
|
'minecraft:stone_pickaxe',
|
|
'minecraft:stone_axe',
|
|
'minecraft:stone_hoe',
|
|
'minecraft:iron_shovel',
|
|
'minecraft:iron_pickaxe',
|
|
'minecraft:iron_axe',
|
|
'minecraft:iron_hoe',
|
|
'minecraft:diamond_shovel',
|
|
'minecraft:diamond_pickaxe',
|
|
'minecraft:diamond_axe',
|
|
'minecraft:diamond_hoe',
|
|
'minecraft:gold_shovel',
|
|
'minecraft:gold_pickaxe',
|
|
'minecraft:gold_axe',
|
|
'minecraft:gold_hoe',
|
|
'minecraft:netherite_shovel',
|
|
'minecraft:netherite_pickaxe',
|
|
'minecraft:netherite_axe',
|
|
'minecraft:netherite_hoe',
|
|
]
|
|
const BREAKABLE = [...MaxDamageItems.keys()]
|
|
const WEARABLE = [
|
|
...ARMOR,
|
|
'minecraft:elytra',
|
|
'minecraft:carved_pumpkin',
|
|
'minecraft:creeper_head',
|
|
'minecraft:dragon_head',
|
|
'minecraft:player_head',
|
|
'minecraft:zombie_head',
|
|
]
|
|
|
|
const EnchantmentsCategories = new Map(Object.entries<string[]>({
|
|
armor: ARMOR,
|
|
armor_feet: ARMOR_FEET,
|
|
armor_legs: ARMOR_LEGS,
|
|
armor_chest: ARMOR_CHEST,
|
|
armor_head: ARMOR_HEAD,
|
|
weapon: SWORD,
|
|
digger: DIGGER,
|
|
fishing_rod: ['minecraft:fishing_rod'],
|
|
trident: ['minecraft:trident'],
|
|
breakable: BREAKABLE,
|
|
bow: ['minecraft:bow'],
|
|
wearable: WEARABLE,
|
|
crossbow: ['minecraft:crossbow'],
|
|
vanishable: [...BREAKABLE, 'minecraft:compass'],
|
|
}))
|