feat:移除了弹窗,服务器添加sls
This commit is contained in:
392
apps/app-frontend/src/lab/recipe-generator/recipe-engine.ts
Normal file
392
apps/app-frontend/src/lab/recipe-generator/recipe-engine.ts
Normal file
@ -0,0 +1,392 @@
|
||||
import { createJavaFormatter, type JavaRecipeFormatter } from './formatter.ts'
|
||||
import { fullId, parseIdentifier } from './identifier.ts'
|
||||
import type {
|
||||
JavaVersionId,
|
||||
RecipeSlot,
|
||||
RecipeSlotContext,
|
||||
RecipeState,
|
||||
SlotValue,
|
||||
} from './types.ts'
|
||||
import {
|
||||
DEFAULT_COOKING_TIME,
|
||||
isVersionAtLeast,
|
||||
supportsRecipeCategory,
|
||||
supportsShowNotification,
|
||||
supportsSmithingTrimPattern,
|
||||
} from './versions.ts'
|
||||
|
||||
export const CRAFTING_GRID_SLOTS: readonly RecipeSlot[] = [
|
||||
'crafting.1',
|
||||
'crafting.2',
|
||||
'crafting.3',
|
||||
'crafting.4',
|
||||
'crafting.5',
|
||||
'crafting.6',
|
||||
'crafting.7',
|
||||
'crafting.8',
|
||||
'crafting.9',
|
||||
]
|
||||
|
||||
const TWO_BY_TWO_DISABLED_INDICES = new Set([2, 5, 6, 7, 8])
|
||||
|
||||
const PATTERN_CHARACTERS = ['#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', ...'abcdefghijklmnopqrstuvwxyz']
|
||||
|
||||
const DINNERBONE_RULES: { char: string; keywords: string[] }[] = [
|
||||
{ char: '/', keywords: ['stick', 'rod', 'torch', 'arrow', 'bone'] },
|
||||
{ char: '_', keywords: ['slab', 'carpet', 'paper', 'map'] },
|
||||
{ char: '=', keywords: ['ingot', 'brick'] },
|
||||
{ char: '.', keywords: ['nugget', 'dust', 'powder', 'seed', 'redstone'] },
|
||||
{ char: 'o', keywords: ['diamond', 'emerald', 'quartz', 'shard', 'pearl', 'ball', 'egg'] },
|
||||
{ char: '~', keywords: ['string', 'vine'] },
|
||||
{ char: ')', keywords: ['bow'] },
|
||||
{ char: 'u', keywords: ['bucket', 'bottle'] },
|
||||
]
|
||||
|
||||
export type ResolvedSlotValue = {
|
||||
ref: ReturnType<typeof parseIdentifier>
|
||||
isTag: boolean
|
||||
}
|
||||
|
||||
export function resolveSlotValue(
|
||||
value: SlotValue | undefined,
|
||||
ctx: RecipeSlotContext,
|
||||
): ResolvedSlotValue | undefined {
|
||||
if (!value) return undefined
|
||||
switch (value.kind) {
|
||||
case 'item':
|
||||
return { ref: parseIdentifier(value.id), isTag: false }
|
||||
case 'vanilla_tag':
|
||||
return { ref: parseIdentifier(value.id), isTag: true }
|
||||
case 'custom_item': {
|
||||
const custom = ctx.customItemsByUid[value.uid]
|
||||
return custom ? { ref: parseIdentifier(custom.id), isTag: false } : undefined
|
||||
}
|
||||
case 'custom_tag': {
|
||||
const custom = ctx.customTagsByUid[value.uid]
|
||||
return custom ? { ref: parseIdentifier(custom.id), isTag: true } : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function slotCount(value: SlotValue | undefined): number | undefined {
|
||||
return value && (value.kind === 'item' || value.kind === 'custom_item') ? value.count : undefined
|
||||
}
|
||||
|
||||
function dinnerboneChallenge(path: string, isTag: boolean): string | null {
|
||||
if (isTag) return null
|
||||
const normalized = path.toLowerCase()
|
||||
for (const rule of DINNERBONE_RULES) {
|
||||
if (rule.keywords.some((keyword) => normalized.includes(keyword))) return rule.char
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function pickKeyName(
|
||||
path: string,
|
||||
value: SlotValue,
|
||||
ctx: RecipeSlotContext,
|
||||
usedKeys: Set<string>,
|
||||
): string {
|
||||
const resolved = resolveSlotValue(value, ctx)
|
||||
const candidates = [dinnerboneChallenge(path, resolved?.isTag === true)]
|
||||
for (const word of path.match(/[a-zA-Z]+/g) ?? []) {
|
||||
candidates.push(word[0].toUpperCase(), word[0].toLowerCase())
|
||||
}
|
||||
for (const letter of path.match(/[a-zA-Z]/g) ?? []) {
|
||||
candidates.push(letter.toUpperCase(), letter.toLowerCase())
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && !usedKeys.has(candidate)) return candidate
|
||||
}
|
||||
const next = PATTERN_CHARACTERS.find((candidate) => !usedKeys.has(candidate))
|
||||
if (!next) throw new Error('Ran out of pattern characters')
|
||||
return next
|
||||
}
|
||||
|
||||
function resolveKeyInfo(
|
||||
value: SlotValue,
|
||||
ctx: RecipeSlotContext,
|
||||
): { reverseKey: string; path: string } {
|
||||
const resolved = resolveSlotValue(value, ctx)
|
||||
if (resolved) return { reverseKey: fullId(resolved.ref), path: resolved.ref.id }
|
||||
const uid = value.kind === 'custom_item' || value.kind === 'custom_tag' ? value.uid : ''
|
||||
return { reverseKey: `${value.kind}:${uid}`, path: uid }
|
||||
}
|
||||
|
||||
export function assignCraftingKeys(
|
||||
grid: (SlotValue | undefined)[],
|
||||
ctx: RecipeSlotContext,
|
||||
): { key: Record<string, SlotValue>; reverse: Record<string, string> } {
|
||||
const cells: { reverseKey: string; path: string; value: SlotValue }[] = []
|
||||
const counts = new Map<string, number>()
|
||||
let primary: string | undefined
|
||||
let primaryCount = 0
|
||||
|
||||
for (const value of grid) {
|
||||
if (!value) continue
|
||||
const info = resolveKeyInfo(value, ctx)
|
||||
cells.push({ reverseKey: info.reverseKey, path: info.path, value })
|
||||
const count = (counts.get(info.reverseKey) ?? 0) + 1
|
||||
counts.set(info.reverseKey, count)
|
||||
if (count > primaryCount) {
|
||||
primary = info.reverseKey
|
||||
primaryCount = count
|
||||
}
|
||||
}
|
||||
|
||||
const key: Record<string, SlotValue> = {}
|
||||
const reverse: Record<string, string> = {}
|
||||
const usedKeys = new Set<string>(['#'])
|
||||
|
||||
for (const cell of cells) {
|
||||
if (reverse[cell.reverseKey]) continue
|
||||
const keyName =
|
||||
cell.reverseKey === primary ? '#' : pickKeyName(cell.path, cell.value, ctx, usedKeys)
|
||||
key[keyName] = cell.value
|
||||
reverse[cell.reverseKey] = keyName
|
||||
usedKeys.add(keyName)
|
||||
}
|
||||
|
||||
return { key, reverse }
|
||||
}
|
||||
|
||||
export function buildPattern(
|
||||
grid: (SlotValue | undefined)[],
|
||||
reverse: Record<string, string>,
|
||||
ctx: RecipeSlotContext,
|
||||
keepWhitespace: boolean,
|
||||
): string[] {
|
||||
const pattern: string[] = []
|
||||
for (const [index, value] of grid.entries()) {
|
||||
const rowIndex = Math.floor(index / 3)
|
||||
pattern[rowIndex] = pattern[rowIndex] ?? ''
|
||||
if (!value) {
|
||||
pattern[rowIndex] += ' '
|
||||
continue
|
||||
}
|
||||
const { reverseKey } = resolveKeyInfo(value, ctx)
|
||||
pattern[rowIndex] += reverse[reverseKey] ?? '#'
|
||||
}
|
||||
|
||||
if (keepWhitespace) return pattern
|
||||
|
||||
while (pattern.length > 0 && pattern[0].trim() === '') pattern.shift()
|
||||
while (pattern.length > 0 && pattern[pattern.length - 1].trim() === '') pattern.pop()
|
||||
if (pattern.length === 0) return pattern
|
||||
|
||||
let minColumn = Number.POSITIVE_INFINITY
|
||||
let maxColumn = 0
|
||||
for (const row of pattern) {
|
||||
let firstNonWhitespace = -1
|
||||
let lastNonWhitespace = -1
|
||||
for (let index = 0; index < row.length; index += 1) {
|
||||
if (row[index] === ' ') continue
|
||||
if (firstNonWhitespace === -1) firstNonWhitespace = index
|
||||
lastNonWhitespace = index
|
||||
}
|
||||
if (firstNonWhitespace === -1) continue
|
||||
minColumn = Math.min(minColumn, firstNonWhitespace)
|
||||
maxColumn = Math.max(maxColumn, lastNonWhitespace + 1)
|
||||
}
|
||||
return pattern.map((row) => row.slice(minColumn, maxColumn))
|
||||
}
|
||||
|
||||
function craftingGrid(state: RecipeState): (SlotValue | undefined)[] {
|
||||
return CRAFTING_GRID_SLOTS.map((slot, index) => {
|
||||
const value = state.slots[slot]
|
||||
if (state.crafting.twoByTwo && TWO_BY_TWO_DISABLED_INDICES.has(index)) return undefined
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
function buildCrafting(
|
||||
state: RecipeState,
|
||||
version: JavaVersionId,
|
||||
ctx: RecipeSlotContext,
|
||||
fmt: JavaRecipeFormatter,
|
||||
): Record<string, unknown> {
|
||||
const grid = craftingGrid(state)
|
||||
const populated = grid.filter((value): value is SlotValue => Boolean(value))
|
||||
const result = state.slots['crafting.result']
|
||||
const resolvedResult = result ? resolveSlotValue(result, ctx) : undefined
|
||||
const resultCount = slotCount(result)
|
||||
const group = state.group.length > 0 ? state.group : undefined
|
||||
const category =
|
||||
supportsRecipeCategory(version, 'crafting') && state.category.length > 0
|
||||
? state.category
|
||||
: undefined
|
||||
const showNotification =
|
||||
supportsShowNotification(version, 'crafting', state.crafting.shapeless) &&
|
||||
state.showNotification === false
|
||||
? { show_notification: false }
|
||||
: {}
|
||||
const output: Record<string, unknown> = {
|
||||
type: fmt.typeName(state.crafting.shapeless ? 'crafting_shapeless' : 'crafting_shaped'),
|
||||
...(category ? { category } : {}),
|
||||
...showNotification,
|
||||
}
|
||||
|
||||
if (state.crafting.shapeless) {
|
||||
return {
|
||||
...output,
|
||||
ingredients: populated.map((value) => {
|
||||
const resolved = resolveSlotValue(value, ctx)
|
||||
return fmt.ingredient(resolved!.ref, resolved!.isTag)
|
||||
}),
|
||||
...(group ? { group } : {}),
|
||||
result:
|
||||
resolvedResult && !resolvedResult.isTag ? fmt.result(resolvedResult.ref, resultCount) : {},
|
||||
}
|
||||
}
|
||||
|
||||
const { key, reverse } = assignCraftingKeys(grid, ctx)
|
||||
return {
|
||||
...output,
|
||||
pattern: buildPattern(grid, reverse, ctx, state.crafting.keepWhitespace),
|
||||
key: Object.fromEntries(
|
||||
Object.entries(key).map(([keyName, value]) => {
|
||||
const resolved = resolveSlotValue(value, ctx)
|
||||
return [keyName, fmt.ingredient(resolved!.ref, resolved!.isTag)]
|
||||
}),
|
||||
),
|
||||
...(group ? { group } : {}),
|
||||
result:
|
||||
resolvedResult && !resolvedResult.isTag ? fmt.result(resolvedResult.ref, resultCount) : {},
|
||||
}
|
||||
}
|
||||
|
||||
function buildCooking(
|
||||
state: RecipeState,
|
||||
version: JavaVersionId,
|
||||
ctx: RecipeSlotContext,
|
||||
fmt: JavaRecipeFormatter,
|
||||
type: 'smelting' | 'blasting' | 'smoking' | 'campfire_cooking',
|
||||
): Record<string, unknown> {
|
||||
const ingredient = resolveSlotValue(state.slots['cooking.ingredient'], ctx)
|
||||
const result = resolveSlotValue(state.slots['cooking.result'], ctx)
|
||||
const group = state.group.length > 0 ? state.group : undefined
|
||||
const category =
|
||||
supportsRecipeCategory(version, state.recipeType) && state.category.length > 0
|
||||
? state.category
|
||||
: undefined
|
||||
const showNotification =
|
||||
supportsShowNotification(version, state.recipeType, false) && state.showNotification === false
|
||||
? { show_notification: false }
|
||||
: {}
|
||||
return {
|
||||
type: fmt.typeName(type),
|
||||
...(category ? { category } : {}),
|
||||
...(group ? { group } : {}),
|
||||
...showNotification,
|
||||
experience: state.cooking.experience,
|
||||
cookingtime: state.cooking.time ?? DEFAULT_COOKING_TIME[type],
|
||||
ingredient: ingredient ? fmt.ingredient(ingredient.ref, ingredient.isTag) : {},
|
||||
result:
|
||||
result && !result.isTag
|
||||
? fmt.cookingResult(result.ref, slotCount(state.slots['cooking.result']))
|
||||
: {},
|
||||
}
|
||||
}
|
||||
|
||||
function buildStonecutter(
|
||||
state: RecipeState,
|
||||
version: JavaVersionId,
|
||||
ctx: RecipeSlotContext,
|
||||
fmt: JavaRecipeFormatter,
|
||||
): Record<string, unknown> {
|
||||
const ingredient = resolveSlotValue(state.slots['stonecutter.ingredient'], ctx)
|
||||
const result = resolveSlotValue(state.slots['stonecutter.result'], ctx)
|
||||
const group =
|
||||
!isVersionAtLeast(version, '26.1') && state.group.length > 0 ? state.group : undefined
|
||||
const showNotification =
|
||||
supportsShowNotification(version, 'stonecutter', false) && state.showNotification === false
|
||||
? { show_notification: false }
|
||||
: {}
|
||||
return {
|
||||
type: fmt.typeName('stonecutting'),
|
||||
...(group ? { group } : {}),
|
||||
...showNotification,
|
||||
ingredient: ingredient ? fmt.ingredient(ingredient.ref, ingredient.isTag) : {},
|
||||
...fmt.stonecutterResult(
|
||||
result?.ref ?? { namespace: 'minecraft', id: 'air' },
|
||||
slotCount(state.slots['stonecutter.result']),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function buildSmithing(
|
||||
state: RecipeState,
|
||||
version: JavaVersionId,
|
||||
ctx: RecipeSlotContext,
|
||||
fmt: JavaRecipeFormatter,
|
||||
): Record<string, unknown> {
|
||||
const template = resolveSlotValue(state.slots['smithing.template'], ctx)
|
||||
const base = resolveSlotValue(state.slots['smithing.base'], ctx)
|
||||
const addition = resolveSlotValue(state.slots['smithing.addition'], ctx)
|
||||
const result = resolveSlotValue(state.slots['smithing.result'], ctx)
|
||||
|
||||
if (state.recipeType === 'smithing') {
|
||||
return {
|
||||
type: fmt.typeName('smithing'),
|
||||
result: result && !result.isTag ? fmt.result(result.ref) : {},
|
||||
base: base ? fmt.ingredient(base.ref, base.isTag) : {},
|
||||
addition: addition ? fmt.ingredient(addition.ref, addition.isTag) : {},
|
||||
}
|
||||
}
|
||||
|
||||
const showNotification =
|
||||
supportsShowNotification(version, state.recipeType, false) && state.showNotification === false
|
||||
? { show_notification: false }
|
||||
: {}
|
||||
|
||||
if (state.recipeType === 'smithing_trim') {
|
||||
return {
|
||||
type: fmt.typeName('smithing_trim'),
|
||||
...showNotification,
|
||||
template: template ? fmt.ingredient(template.ref, template.isTag) : {},
|
||||
base: base ? fmt.ingredient(base.ref, base.isTag) : {},
|
||||
addition: addition ? fmt.ingredient(addition.ref, addition.isTag) : {},
|
||||
...(supportsSmithingTrimPattern(version) && state.smithing.trimPattern
|
||||
? { pattern: state.smithing.trimPattern }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: fmt.typeName('smithing_transform'),
|
||||
...showNotification,
|
||||
template: template ? fmt.ingredient(template.ref, template.isTag) : {},
|
||||
base: base ? fmt.ingredient(base.ref, base.isTag) : {},
|
||||
addition: addition ? fmt.ingredient(addition.ref, addition.isTag) : {},
|
||||
result: result && !result.isTag ? fmt.result(result.ref) : {},
|
||||
}
|
||||
}
|
||||
|
||||
export function generateJavaRecipe(
|
||||
state: RecipeState,
|
||||
version: JavaVersionId,
|
||||
ctx: RecipeSlotContext,
|
||||
): Record<string, unknown> {
|
||||
const fmt = createJavaFormatter(version)
|
||||
switch (state.recipeType) {
|
||||
case 'crafting':
|
||||
return buildCrafting(state, version, ctx, fmt)
|
||||
case 'smelting':
|
||||
case 'blasting':
|
||||
case 'smoking':
|
||||
case 'campfire_cooking':
|
||||
return buildCooking(state, version, ctx, fmt, state.recipeType)
|
||||
case 'stonecutter':
|
||||
return buildStonecutter(state, version, ctx, fmt)
|
||||
case 'smithing':
|
||||
case 'smithing_trim':
|
||||
case 'smithing_transform':
|
||||
return buildSmithing(state, version, ctx, fmt)
|
||||
default:
|
||||
throw new Error(`Unsupported recipe type: ${state.recipeType satisfies never}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getCraftingGridValues(state: RecipeState): (SlotValue | undefined)[] {
|
||||
return craftingGrid(state)
|
||||
}
|
||||
Reference in New Issue
Block a user