diff --git a/demo/main.ts b/demo/main.ts index 00b2a7e..9e35bfd 100644 --- a/demo/main.ts +++ b/demo/main.ts @@ -1,6 +1,6 @@ import { mat4 } from 'gl-matrix' import type { ItemRendererResources, ItemRenderingContext, NbtTag, Resources, Voxel } from '../src/index.js' -import { BlockDefinition, BlockModel, Identifier, Item, ItemRenderer, ItemStack, NormalNoise, Structure, StructureRenderer, TextureAtlas, VoxelRenderer, XoroshiroRandom, jsonToNbt, upperPowerOfTwo } from '../src/index.js' +import { BlockDefinition, BlockModel, Identifier, ItemRenderer, ItemStack, jsonToNbt, NormalNoise, Structure, StructureRenderer, TextureAtlas, upperPowerOfTwo, VoxelRenderer, XoroshiroRandom } from '../src/index.js' import { } from '../src/nbt/Util.js' import { ItemModel } from '../src/render/ItemModel.js' @@ -107,13 +107,13 @@ Promise.all([ itemModels['minecraft:' + id] = ItemModel.fromJson(item_models[id].model) }) - + const itemComponents: Record> = {} Object.keys(item_components).forEach(id => { const components = new Map() Object.keys(item_components[id]).forEach(c_id => { components.set(c_id, jsonToNbt(item_components[id][c_id])) }) - Item.REGISTRY.register(Identifier.create(id), new Item(components)) + itemComponents['minecraft:' + id] = components }) const atlasCanvas = document.createElement('canvas') @@ -140,12 +140,13 @@ Promise.all([ getBlockProperties(id) { return null }, getDefaultBlockProperties(id) { return null }, getItemModel(id) { return itemModels[id.toString()] }, + getItemComponents(id) { return itemComponents[id.toString()] }, } // === Item rendering === const context: ItemRenderingContext = { - "bundle/selected_item": 0 + 'bundle/selected_item': 0, } const itemCanvas = document.getElementById('item-display') as HTMLCanvasElement @@ -159,7 +160,7 @@ Promise.all([ try { const id = itemInput.value const itemStack = ItemStack.fromString(itemInput.value) - itemGl.clear(itemGl.DEPTH_BUFFER_BIT | itemGl.COLOR_BUFFER_BIT); + itemGl.clear(itemGl.DEPTH_BUFFER_BIT | itemGl.COLOR_BUFFER_BIT) itemRenderer.setItem(itemStack, context) itemRenderer.drawItem() itemInput.classList.remove('invalid') diff --git a/src/core/Item.ts b/src/core/Item.ts deleted file mode 100644 index 22d5f1f..0000000 --- a/src/core/Item.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NbtTag } from "../nbt/index.js" -import { Identifier } from "./index.js" -import { Registry } from "./Registry.js" - - -export class Item { - public static REGISTRY = Registry.createAndRegister('item') - - constructor( - public components: Map = new Map(), - ) { - } - - public getComponent(key: string | Identifier, reader: (tag: NbtTag) => T) { - if (typeof key === 'string') { - key = Identifier.parse(key) - } - const value = this.components.get(key.toString()) - if (value) { - return reader(value) - } - return undefined - } - - public hasComponent(key: string | Identifier) { - if (typeof key === 'string') { - key = Identifier.parse(key) - } - return this.components.has(key.toString()) - } -} \ No newline at end of file diff --git a/src/core/ItemStack.ts b/src/core/ItemStack.ts index 8bd829c..ca2ea05 100644 --- a/src/core/ItemStack.ts +++ b/src/core/ItemStack.ts @@ -1,23 +1,21 @@ -import { NbtParser } from '../nbt/NbtParser.js' import type { NbtTag } from '../nbt/index.js' import { NbtCompound, NbtInt, NbtString } from '../nbt/index.js' +import { NbtParser } from '../nbt/NbtParser.js' import { StringReader } from '../util/index.js' -import { Holder } from './Holder.js' import { Identifier } from './Identifier.js' -import { Item } from './Item.js' -export class ItemStack { - private readonly item: Holder +export interface ItemComponentsProvider { + getItemComponents: (id: Identifier) => Map, +} +export class ItemStack { constructor( public readonly id: Identifier, public count: number, public readonly components: Map = new Map(), - ) { - this.item = Holder.reference(Item.REGISTRY, id, false) - } + ) {} - public getComponent(key: string | Identifier, reader: (tag: NbtTag) => T, includeDefaultComponents: boolean = true): T | undefined { + public getComponent(key: string | Identifier, baseComponents?: ItemComponentsProvider): NbtTag | undefined { if (typeof key === 'string') { key = Identifier.parse(key) } @@ -27,20 +25,28 @@ export class ItemStack { } const value = this.components.get(key.toString()) if (value) { - return reader(value) + return value } - return includeDefaultComponents ? this.item.value()?.getComponent(key, reader) : undefined + if (baseComponents) { + return baseComponents.getItemComponents(this.id)?.get(key.toString()) + } + return undefined } - public hasComponent(key: string | Identifier, includeDefaultComponents: boolean = true): boolean { + public hasComponent(key: string | Identifier, baseComponents?: ItemComponentsProvider): boolean { if (typeof key === 'string') { key = Identifier.parse(key) } if (this.components.has('!' + key.toString())){ return false } - - return this.components.has(key.toString()) || (includeDefaultComponents && (this.item.value()?.hasComponent(key) ?? false)) + if (this.components.has(key.toString())) { + return true + } + if (baseComponents) { + return baseComponents.getItemComponents(this.id)?.has(key.toString()) + } + return false } public clone(): ItemStack { @@ -85,7 +91,9 @@ export class ItemStack { public toString() { let result = this.id.toString() if (this.components.size > 0) { - result += `[${[...this.components.entries()].map(([k, v]) => `${k}=${v.toString()}`).join(',')}]` + result += `[${[...this.components.entries()].map(([k, v]) => { + return k.startsWith('!') ? k : `${k}=${v.toString()}` + }).join(',')}]` } if (this.count > 1) { result += ` ${this.count}` @@ -123,12 +131,12 @@ export class ItemStack { reader.skip() } const component = Identifier.parse(reader.getRead(start)).toString() - if (!reader.canRead()) break; + if (!reader.canRead()) break reader.skip() const tag = NbtParser.readTag(reader) components.set(component, tag) } - if (!reader.canRead()) break; + if (!reader.canRead()) break if (reader.peek() === ']'){ return new ItemStack(itemId, 1, components) } @@ -156,7 +164,12 @@ export class ItemStack { const id = Identifier.parse(nbt.getString('id')) const count = nbt.hasNumber('count') ? nbt.getNumber('count') : 1 const components = new Map(Object.entries( - nbt.getCompound('components').map((key, value) => [Identifier.parse(key).toString(), value]) + nbt.getCompound('components').map((key, value) => { + if (key.startsWith('!')) { + return ['!' + Identifier.parse(key).toString(), new NbtCompound()] + } + return [Identifier.parse(key).toString(), value] + }) )) return new ItemStack(id, count, components) } diff --git a/src/core/index.ts b/src/core/index.ts index fba2592..bdb8c1f 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -8,11 +8,9 @@ export * from './Effects.js' export * from './Holder.js' export * from './HolderSet.js' export * from './Identifier.js' -export * from './Item.js' export * from './ItemStack.js' export * from './PalettedContainer.js' export * from './Registry.js' export * from './Rotation.js' export * from './Structure.js' export * from './StructureProvider.js' - diff --git a/src/render/ItemModel.ts b/src/render/ItemModel.ts index 7dde809..2532106 100644 --- a/src/render/ItemModel.ts +++ b/src/render/ItemModel.ts @@ -1,9 +1,12 @@ -import { Identifier, ItemStack, } from "../core/index.js" -import { clamp } from "../math/index.js" -import { Color, Json } from "../util/index.js" -import { ItemTint } from "./ItemTint.js" -import { SpecialModel } from "./SpecialModel.js" -import { Cull, ItemRenderer, ItemRendererResources, ItemRenderingContext, Mesh } from "./index.js" +import { Identifier, ItemStack } from '../core/index.js' +import { clamp } from '../math/index.js' +import type { Color } from '../util/index.js' +import { Json } from '../util/index.js' +import { Cull } from './Cull.js' +import type { ItemRendererResources, ItemRenderingContext } from './index.js' +import { ItemTint } from './ItemTint.js' +import { Mesh } from './Mesh.js' +import { SpecialModel } from './SpecialModel.js' export interface ItemModelProvider { getItemModel(id: Identifier): ItemModel | null @@ -70,8 +73,8 @@ export namespace ItemModel { export class Model extends ItemModel { constructor( - private modelId: Identifier, - private tints: ItemTint[] + private readonly modelId: Identifier, + private readonly tints: ItemTint[] ) { super() } @@ -82,7 +85,7 @@ export namespace ItemModel { throw new Error(`Model ${this.modelId} does not exist (trying to render ${item.toString()})`) } - let tint = (i: number): Color => { + const tint = (i: number): Color => { if (i < this.tints.length) { return this.tints[i].getTint(item, context) } else { @@ -99,7 +102,7 @@ export namespace ItemModel { export class Composite extends ItemModel { constructor( - private models: ItemModel[] + private readonly models: ItemModel[] ) { super() } @@ -113,9 +116,9 @@ export namespace ItemModel { export class Condition extends ItemModel { constructor( - private property: (item: ItemStack, context: ItemRenderingContext) => boolean, - private onTrue: ItemModel, - private onFalse: ItemModel + private readonly property: (item: ItemStack, context: ItemRenderingContext) => boolean, + private readonly onTrue: ItemModel, + private readonly onFalse: ItemModel ) { super() } @@ -140,29 +143,30 @@ export namespace ItemModel { case 'bundle/has_selected_item': return (item, context) => (context['bundle/selected_item'] ?? -1) >= 0 case 'broken': return (item, context) => { - const damage = item.getComponent('damage', tag => tag.getAsNumber()) - const max_damage = item.getComponent('max_damage', tag => tag.getAsNumber()) + const damage = item.getComponent('damage')?.getAsNumber() + const max_damage = item.getComponent('max_damage')?.getAsNumber() return (damage !== undefined && max_damage !== undefined && damage >= max_damage - 1) } case 'damaged': return (item, context) => { - const damage = item.getComponent('damage', tag => tag.getAsNumber()) - const max_damage = item.getComponent('max_damage', tag => tag.getAsNumber()) - return (damage !== undefined && max_damage !== undefined && damage >= 1) - } + const damage = item.getComponent('damage')?.getAsNumber() + const max_damage = item.getComponent('max_damage')?.getAsNumber() + return (damage !== undefined && max_damage !== undefined && damage >= 1) + } case 'has_component': const componentId = Identifier.parse(Json.readString(root.component) ?? '') const ignore_default = Json.readBoolean(root.ignore_default) ?? false - return (item, context) => item.hasComponent(componentId, !ignore_default) + return (item, context) => item.hasComponent(componentId) case 'keybind_down': const keybind = Json.readString(root.keybind) ?? '' return (item, context) => context.keybind_down?.includes(keybind) ?? false case 'custom_model_data': const index = Json.readInt(root.index) ?? 0 - return (item, context) => item.getComponent('custom_model_data', tag => { - if (!tag.isCompound()) return false + return (item, context) => { + const tag = item.getComponent('custom_model_data') + if (!tag?.isCompound()) return false const flag = tag.getList('flags').getNumber(index) return flag !== undefined && flag !== 0 - }) ?? false + } default: throw new Error(`Invalid condition property ${property}`) } @@ -171,9 +175,9 @@ export namespace ItemModel { export class Select extends ItemModel { constructor( - private property: (item: ItemStack, context: ItemRenderingContext) => string | null, - private cases: Map, - private fallback?: ItemModel + private readonly property: (item: ItemStack, context: ItemRenderingContext) => string | null, + private readonly cases: Map, + private readonly fallback?: ItemModel ) { super() } @@ -195,43 +199,51 @@ export namespace ItemModel { return (item, context) => context.context_dimension?.toString() ?? null case 'charge_type': const FIREWORK = Identifier.create('firework_rocket') - return (item, context) => item.getComponent('charged_projectiles', tag => { - if (!tag.isList() || tag.length === 0) { + return (item, context) => { + const tag = item.getComponent('charged_projectiles') + if (!tag?.isList() || tag.length === 0) { return 'none' } return tag.filter(tag => { if (!tag.isCompound()) { return false - } + } return Identifier.parse(tag.getString('id')).equals(FIREWORK) }).length > 0 ? 'rocket' : 'arrow' - }) ?? 'none' + } case 'trim_material': - return (item, context) => item.getComponent('trim', tag => { - if (!tag.isCompound()) { - return undefined + return (item, context) => { + const tag = item.getComponent('trim') + if (!tag?.isCompound()) { + return null } return Identifier.parse(tag.getString('material')).toString() - }) ?? null + } case 'block_state': const block_state_property = Json.readString(root.block_state_property) ?? '' - return (item, context) => item.getComponent('block_state', tag => { - if (!tag.isCompound()) { - return undefined + return (item, context) => { + const tag = item.getComponent('block_state') + if (!tag?.isCompound()) { + return null } return tag.getString(block_state_property) - }) ?? null + } case 'local_time': return (item, context) => 'NOT IMPLEMENTED' case 'context_entity_type': return (item, context) => context.context_entity_type?.toString() ?? null case 'custom_model_data': const index = Json.readInt(root.index) ?? 0 - return (item, context) => item.getComponent('custom_model_data', tag => { - if (!tag.isCompound()) return undefined + return (item, context) => { + const tag = item.getComponent('custom_model_data') + if (!tag?.isCompound()) { + return null + } const list = tag.getList('strings') - if (list.length <= index) return undefined + if (list.length <= index) { + return null + } return list.getString(index) - }) ?? null + } default: throw new Error(`Invalid select property ${property}`) @@ -240,13 +252,13 @@ export namespace ItemModel { } export class RangeDispatch extends ItemModel { - private entries: {threshold: number, model: ItemModel}[] + private readonly entries: {threshold: number, model: ItemModel}[] constructor( - private property: (item: ItemStack, context: ItemRenderingContext) => number, - private scale: number, + private readonly property: (item: ItemStack, context: ItemRenderingContext) => number, + private readonly scale: number, entries: {threshold: number, model: ItemModel}[], - private fallback?: ItemModel + private readonly fallback?: ItemModel ) { super() this.entries = entries.sort((a, b) => a.threshold - b.threshold) @@ -265,22 +277,30 @@ export namespace ItemModel { return model?.getMesh(item, resources, context) ?? MISSING_MESH } - static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => number{ + static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => number { const property = Json.readString(root.property)?.replace(/^minecraft:/, '') switch (property){ case 'bundle/fullness': - function calculateBundleWeight(item: ItemStack): number{ - const bundle_contents = item.getComponent('bundle_contents', tag => { - if (!tag.isListOrArray()) return undefined - return tag.map(t => t.isCompound() ? ItemStack.fromNbt(t) : undefined) - }) - if (bundle_contents === undefined) return 0 - return bundle_contents.reduce((weight, item) => { - if (item === undefined) return weight - if (item.hasComponent('bundle_contents')) return weight + calculateBundleWeight(item) + 1/16 - if (item.getComponent('bees', tag => tag.isListOrArray() && tag.length > 0)) return weight + 1 - return weight + item.count / (item.getComponent('max_stack_size', tag => tag.getAsNumber()) ?? 1) + function calculateBundleWeight(item: ItemStack): number { + const tag = item.getComponent('bundle_contents') + if (!tag?.isListOrArray()) { + return 0 + } + const items = tag.map(t => t.isCompound() ? ItemStack.fromNbt(t) : undefined) + return items.reduce((weight, item) => { + if (item === undefined) { + return weight + } + if (item.hasComponent('bundle_contents')) { + return weight + calculateBundleWeight(item) + 1/16 + } + const beesTag = item.getComponent('bees') + if (beesTag?.isListOrArray() && beesTag.length > 0) { + return weight + 1 + } + const maxStackSize = item.getComponent('max_stack_size')?.getAsNumber() ?? 1 + return weight + item.count / maxStackSize }, 0) } @@ -288,39 +308,38 @@ export namespace ItemModel { case 'damage': { const normalize = Json.readBoolean(root.normalize) ?? true return (item, context) => { - const max_damage = item.getComponent('max_damage', tag => tag.getAsNumber()) ?? 0 - const damage = clamp(item.getComponent('damage', tag => tag.getAsNumber()) ?? 0, 0, max_damage) - if (normalize) return clamp(damage / max_damage, 0, 1) - return clamp(damage, 0, max_damage) + const maxDamage = item.getComponent('max_damage')?.getAsNumber() ?? 0 + const damage = clamp(item.getComponent('damage')?.getAsNumber() ?? 0, 0, maxDamage) + if (normalize) return clamp(damage / maxDamage, 0, 1) + return clamp(damage, 0, maxDamage) } } case 'count': { const normalize = Json.readBoolean(root.normalize) ?? true return (item, context) => { - const max_stack_size = item.getComponent('max_stack_size', tag => tag.getAsNumber()) ?? 1 - if (normalize) return clamp(item.count / max_stack_size, 0, 1) - return clamp(item.count, 0, max_stack_size) + const maxStackSize = item.getComponent('max_stack_size')?.getAsNumber() ?? 1 + if (normalize) return clamp(item.count / maxStackSize, 0, 1) + return clamp(item.count, 0, maxStackSize) } } case 'cooldown': return (item, context) => { - const cooldownGroup = item.getComponent('use_cooldown', tag => { - if (!tag.isCompound()) return undefined - return Identifier.parse(tag.getString('cooldown_group')) - }) ?? item.id - + const tag = item.getComponent('use_cooldown') + const cooldownGroup = tag?.isCompound() + ? Identifier.parse(tag.getString('cooldown_group') ?? item.id) + : item.id return context.cooldown_percentage?.[cooldownGroup.toString()] ?? 0 } case 'time': const source = Json.readString(root.source) ?? 'daytime' switch (source) { - case 'daytime': return (item, context) => { - const gameTime = context.game_time ?? 0 - const linearTime = ((gameTime / 24000.0) % 1) - 0.25; - const cosTime = 0.5 - Math.cos(linearTime * Math.PI) / 2.0; - return (linearTime * 2.0 + cosTime) / 3; - } case 'moon_phase': return (item, context) => ((context.game_time ?? 0) / 24000 % 8) / 8 case 'random': return (item, context) => Math.random() + default: return (item, context) => { + const gameTime = context.game_time ?? 0 + const linearTime = ((gameTime / 24000.0) % 1) - 0.25 + const cosTime = 0.5 - Math.cos(linearTime * Math.PI) / 2.0 + return (linearTime * 2.0 + cosTime) / 3 + } } case 'compass': return (item, context) => context.compass_angle ?? 0 // TODO: calculate properly? case 'crossbow/pull': return (item, context) => context['crossbow/pull'] ?? 0 @@ -339,10 +358,13 @@ export namespace ItemModel { } case 'custom_model_data': const index = Json.readInt(root.index) ?? 0 - return (item, context) => item.getComponent('custom_model_data', tag => { - if (!tag.isCompound()) return undefined + return (item, context) => { + const tag = item.getComponent('custom_model_data') + if (!tag?.isCompound()) { + return 0 + } return tag.getList('floats').getNumber(index) - }) ?? 0 + } default: throw new Error(`Invalid select property ${property}`) } @@ -351,8 +373,8 @@ export namespace ItemModel { export class Special extends ItemModel { constructor( - private specialModel: SpecialModel, - private base: Identifier + private readonly specialModel: SpecialModel, + private readonly base: Identifier ) { super() } @@ -372,21 +394,23 @@ export namespace ItemModel { public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh { const selectedItemIndex = context['bundle/selected_item'] if (selectedItemIndex === undefined || selectedItemIndex < 0) return new Mesh() - const selectedItem = item.getComponent('bundle_contents', tag => { - if (!tag.isListOrArray()) return undefined - const selectedItemTag = tag.get(selectedItemIndex) - if (selectedItemTag === undefined || !selectedItemTag.isCompound()) return undefined - return ItemStack.fromNbt(selectedItemTag) - }) - - return selectedItem !== undefined ? ItemRenderer.getItemMesh(selectedItem, resources, { - ...context, - 'bundle/selected_item': -1, - selected: false, - carried: false, - use_duration: -1 - }) : new Mesh() + const tag = item.getComponent('bundle_contents') + if (!tag?.isListOrArray()) { + return new Mesh() + } + const selectedItemTag = tag.get(selectedItemIndex) + if (selectedItemTag === undefined || !selectedItemTag.isCompound()) { + return new Mesh() + } + const selectedItem = ItemStack.fromNbt(selectedItemTag) + return new Mesh() + // return ItemRenderer.getItemMesh(selectedItem, resources, { + // ...context, + // 'bundle/selected_item': -1, + // selected: false, + // carried: false, + // use_duration: -1, + // }) } } } - diff --git a/src/render/ItemRenderer.ts b/src/render/ItemRenderer.ts index 00594af..fe85f9e 100644 --- a/src/render/ItemRenderer.ts +++ b/src/render/ItemRenderer.ts @@ -1,14 +1,14 @@ import { mat4 } from 'gl-matrix' -import { ItemStack } from '../core/ItemStack.js' import { Identifier } from '../core/index.js' -import { Color } from '../index.js' +import type { ItemComponentsProvider, ItemStack } from '../core/ItemStack.js' +import type { Color } from '../index.js' import type { BlockModelProvider, Display } from './BlockModel.js' -import { ItemModelProvider } from './ItemModel.js' +import type { ItemModelProvider } from './ItemModel.js' import { Mesh } from './Mesh.js' import { Renderer } from './Renderer.js' import type { TextureAtlasProvider } from './TextureAtlas.js' -export interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider, ItemModelProvider {} +export interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider, ItemModelProvider, ItemComponentsProvider {} export type ItemRenderingContext = { display_context?: Display, @@ -18,21 +18,21 @@ export type ItemRenderingContext = { selected?: boolean, carried?: boolean, extended_view?: boolean, - context_entity_is_view_entity?: boolean + context_entity_is_view_entity?: boolean, keybind_down?: string[], main_hand?: 'left' | 'right', context_entity_type?: Identifier, - context_entity_team_color?: Color - context_dimension?: Identifier + context_entity_team_color?: Color, + context_dimension?: Identifier, cooldown_percentage?: {[key: string]: number}, game_time?: number, compass_angle?: number, use_duration?: number, max_use_duration?: number, - 'crossbow/pull'?: number + 'crossbow/pull'?: number, } export class ItemRenderer extends Renderer { @@ -63,7 +63,7 @@ export class ItemRenderer extends Renderer { } public static getItemMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext) { - const itemModelId = item.getComponent('item_model', tag => tag.getAsString()) + const itemModelId = item.getComponent('item_model', resources)?.getAsString() if (itemModelId === undefined){ return new Mesh() } diff --git a/src/render/ItemTint.ts b/src/render/ItemTint.ts index 41c5504..fbb190f 100644 --- a/src/render/ItemTint.ts +++ b/src/render/ItemTint.ts @@ -1,4 +1,5 @@ -import { Color, ItemRenderingContext, ItemStack, Json, NbtIntArray, PotionContents } from "../index.js" +import type { ItemRenderingContext, ItemStack } from '../index.js' +import { Color, Json, PotionContents } from '../index.js' export abstract class ItemTint { public abstract getTint(item: ItemStack, context: ItemRenderingContext): Color @@ -62,11 +63,14 @@ export namespace ItemTint { } public getTint(item: ItemStack): Color { - const dyedColor = item.getComponent('dyed_color', tag => { - return tag.isCompound() ? tag.getNumber('rgb') : tag.getAsNumber() - }) - if (dyedColor === undefined) return this.default_color - return Color.intToRgb(dyedColor) + const tag = item.getComponent('dyed_color') + if (!tag) { + return this.default_color + } + if (!tag.isCompound()) { + return Color.intToRgb(tag.getAsNumber()) + } + return Color.intToRgb(tag.getNumber('rgb')) } } @@ -91,16 +95,15 @@ export namespace ItemTint { } public getTint(item: ItemStack): Color { - const colors = item.getComponent('firework_explosion', tag => { - if (!tag.isCompound()) return new NbtIntArray() - const colorsTag = tag.get('colors') - if (colorsTag && colorsTag.isListOrArray()) return colorsTag - return new NbtIntArray() - }) + const tag = item.getComponent('firework_explosion') + if (!tag?.isCompound()) { + return this.default_color + } + const colors = tag.get('colors') + if (!colors || !colors.isListOrArray()) { + return this.default_color + } const color: Color = (() => { - if (!colors || colors.length === 0) { - return this.default_color - } if (colors.length === 1) { return Color.intToRgb(colors.get(0)!.getAsNumber()) } @@ -127,9 +130,12 @@ export namespace ItemTint { } public getTint(item: ItemStack): Color { - const potion_contents = item.getComponent('potion_contents', PotionContents.fromNbt ) - if (!potion_contents) return this.default_color - return PotionContents.getColor(potion_contents) + const tag = item.getComponent('potion_contents') + if (!tag) { + return this.default_color + } + const potionContents = PotionContents.fromNbt(tag) + return PotionContents.getColor(potionContents) } } @@ -141,9 +147,11 @@ export namespace ItemTint { } public getTint(item: ItemStack): Color { - const mapColor = item.getComponent('map_color', tag => tag.getAsNumber()) - if (mapColor === undefined) return this.default_color - return Color.intToRgb(mapColor) + const mapColor = item.getComponent('map_color') + if (!mapColor) { + return this.default_color + } + return Color.intToRgb(mapColor.getAsNumber()) } } @@ -156,12 +164,15 @@ export namespace ItemTint { } public getTint(item: ItemStack): Color { - return item.getComponent('custom_model_data', tag => { - if (!tag.isCompound()) return undefined - const colorTag = tag.getList('colors').get(this.index) - if (colorTag === undefined) return undefined - return Color.fromNbt(colorTag) - }) ?? this.default_color + const tag = item.getComponent('custom_model_data') + if (!tag?.isCompound()) { + return this.default_color + } + const colors = tag.getList('colors').get(this.index) + if (!colors) { + return this.default_color + } + return Color.fromNbt(colors) ?? this.default_color } } @@ -176,4 +187,4 @@ export namespace ItemTint { return context.context_entity_team_color ?? this.default_color } } -} \ No newline at end of file +} diff --git a/src/render/index.ts b/src/render/index.ts index a679b60..4cc57dd 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -3,7 +3,9 @@ export * from './BlockDefinition.js' export * from './BlockModel.js' export * from './ChunkBuilder.js' export * from './Cull.js' +export * from './ItemModel.js' export * from './ItemRenderer.js' +export * from './ItemTint.js' export * from './Line.js' export * from './Mesh.js' export * from './Quad.js' @@ -14,4 +16,3 @@ export * from './StructureRenderer.js' export * from './TextureAtlas.js' export * from './Vertex.js' export * from './VoxelRenderer.js' - diff --git a/src/util/Color.ts b/src/util/Color.ts index 08ddbd7..b5678c0 100644 --- a/src/util/Color.ts +++ b/src/util/Color.ts @@ -1,5 +1,5 @@ -import { NbtTag } from "../nbt/index.js" -import { Json } from "./Json.js" +import type { NbtTag } from '../nbt/index.js' +import { Json } from './Json.js' export type Color = [number, number, number] @@ -26,4 +26,4 @@ export namespace Color { const b = n & 255 return [r / 255, g / 255, b / 255] } -} \ No newline at end of file +} diff --git a/test/render/ItemModel.test.ts b/test/render/ItemModel.test.ts index caa6110..9a13d3b 100644 --- a/test/render/ItemModel.test.ts +++ b/test/render/ItemModel.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import { BlockModel, Color, Identifier, ItemRendererResources, ItemStack } from '../../src' +import type { Color, ItemRendererResources } from '../../src' +import { BlockModel, Identifier, ItemStack } from '../../src' import { ItemModel } from '../../src/render/ItemModel' describe('ItemModel', () => { @@ -7,7 +8,7 @@ describe('ItemModel', () => { const blockModels = { 'test:1': new BlockModel(undefined, undefined, undefined), - 'test:2': new BlockModel(undefined, undefined, undefined) + 'test:2': new BlockModel(undefined, undefined, undefined), } const blockModel1 = vi.spyOn(blockModels['test:1'], 'getMesh') @@ -18,6 +19,7 @@ describe('ItemModel', () => { getItemModel(id) { return null }, getTextureAtlas() { return new ImageData(0, 0) }, getTextureUV(texture) { return [0, 0, 0, 0] }, + getItemComponents(id) { return new Map() }, } it('Model', () => { @@ -26,10 +28,10 @@ describe('ItemModel', () => { model: 'test:1', tints: [ { - type: "constant", - value: [0.5, 0.6, 0.7] - } - ] + type: 'constant', + value: [0.5, 0.6, 0.7], + }, + ], }) blockModel1.mockClear() @@ -52,7 +54,7 @@ describe('ItemModel', () => { { type: 'model', model: 'test:1', - } + }, ], }) @@ -73,7 +75,7 @@ describe('ItemModel', () => { on_false: { type: 'model', model: 'test:2', - } + }, }) blockModel1.mockClear() @@ -154,12 +156,12 @@ describe('ItemModel', () => { type: 'model', model: 'test:1', }, - } + }, ], fallback: { type: 'model', model: 'test:2', - } + }, }) blockModel1.mockClear() @@ -177,19 +179,19 @@ describe('ItemModel', () => { it('Select properties', () => { const main_hand = ItemModel.Select.propertyFromJson({property: 'main_hand'}) - expect(main_hand(dummyItem, {'main_hand': 'left'})).toEqual('left') - expect(main_hand(dummyItem, {'main_hand': 'right'})).toEqual('right') + expect(main_hand(dummyItem, {main_hand: 'left'})).toEqual('left') + expect(main_hand(dummyItem, {main_hand: 'right'})).toEqual('right') const display_context = ItemModel.Select.propertyFromJson({property: 'display_context'}) - expect(display_context(dummyItem, {'display_context': 'gui'})).toEqual('gui') - expect(display_context(dummyItem, {'display_context': 'fixed'})).toEqual('fixed') + expect(display_context(dummyItem, {display_context: 'gui'})).toEqual('gui') + expect(display_context(dummyItem, {display_context: 'fixed'})).toEqual('fixed') const context_entity_type = ItemModel.Select.propertyFromJson({property: 'context_entity_type'}) - expect(context_entity_type(dummyItem, {'context_entity_type': Identifier.create('zombie')})).toEqual('minecraft:zombie') + expect(context_entity_type(dummyItem, {context_entity_type: Identifier.create('zombie')})).toEqual('minecraft:zombie') expect(context_entity_type(dummyItem, {})).toBeNull() const context_dimension = ItemModel.Select.propertyFromJson({property: 'context_dimension'}) - expect(context_dimension(dummyItem, {'context_dimension': Identifier.parse('test:test')})).toEqual('test:test') + expect(context_dimension(dummyItem, {context_dimension: Identifier.parse('test:test')})).toEqual('test:test') expect(context_dimension(dummyItem, {})).toBeNull() const charge_type = ItemModel.Select.propertyFromJson({property: 'charge_type'}) @@ -226,12 +228,12 @@ describe('ItemModel', () => { type: 'model', model: 'test:1', }, - } + }, ], fallback: { type: 'model', model: 'test:2', - } + }, }) blockModel1.mockClear() @@ -298,4 +300,4 @@ describe('ItemModel', () => { // not testing compass and crossbow/pull as they are not properly implemented }) -}) \ No newline at end of file +}) diff --git a/test/render/ItemTint.test.ts b/test/render/ItemTint.test.ts index 95da081..c116e41 100644 --- a/test/render/ItemTint.test.ts +++ b/test/render/ItemTint.test.ts @@ -77,4 +77,4 @@ describe('ItemTint', () => { }) // not testing grass as its not properly implemented -}) \ No newline at end of file +})