diff --git a/demo/index.html b/demo/index.html
index a4df886..83acbc4 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -20,6 +20,7 @@
input {
padding: 4px;
margin-left: 8px;
+ width: 500px;
}
.invalid {
color: #cb0000;
diff --git a/demo/main.ts b/demo/main.ts
index c7c244f..00b2a7e 100644
--- a/demo/main.ts
+++ b/demo/main.ts
@@ -1,6 +1,8 @@
import { mat4 } from 'gl-matrix'
-import type { Resources, Voxel } from '../src/index.js'
-import { BlockDefinition, BlockModel, Identifier, ItemRenderer, ItemStack, NormalNoise, Structure, StructureRenderer, TextureAtlas, upperPowerOfTwo, VoxelRenderer, XoroshiroRandom } from '../src/index.js'
+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 { } from '../src/nbt/Util.js'
+import { ItemModel } from '../src/render/ItemModel.js'
class InteractiveCanvas {
@@ -66,6 +68,8 @@ Promise.all([
fetch(`${MCMETA}registries/item/data.min.json`).then(r => r.json()),
fetch(`${MCMETA}summary/assets/block_definition/data.min.json`).then(r => r.json()),
fetch(`${MCMETA}summary/assets/model/data.min.json`).then(r => r.json()),
+ fetch(`${MCMETA}summary/assets/item_definition/data.min.json`).then(r => r.json()),
+ fetch(`${MCMETA}summary/item_components/data.min.json`).then(r => r.json()),
fetch(`${MCMETA}atlas/all/data.min.json`).then(r => r.json()),
new Promise(res => {
const image = new Image()
@@ -73,7 +77,7 @@ Promise.all([
image.crossOrigin = 'Anonymous'
image.src = `${MCMETA}atlas/all/atlas.png`
}),
-]).then(([items, blockstates, models, uvMap, atlas]) => {
+]).then(([items, blockstates, models, item_models, item_components, uvMap, atlas]) => {
// === Prepare assets for item and structure rendering ===
@@ -97,6 +101,21 @@ Promise.all([
})
Object.values(blockModels).forEach((m: any) => m.flatten({ getBlockModel: id => blockModels[id] }))
+
+ const itemModels: Record = {}
+ Object.keys(item_models).forEach(id => {
+ itemModels['minecraft:' + id] = ItemModel.fromJson(item_models[id].model)
+ })
+
+
+ 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))
+ })
+
const atlasCanvas = document.createElement('canvas')
const atlasSize = upperPowerOfTwo(Math.max(atlas.width, atlas.height))
atlasCanvas.width = atlasSize
@@ -112,7 +131,7 @@ Promise.all([
})
const textureAtlas = new TextureAtlas(atlasData, idMap)
- const resources: Resources = {
+ const resources: Resources & ItemRendererResources = {
getBlockDefinition(id) { return blockDefinitions[id.toString()] },
getBlockModel(id) { return blockModels[id.toString()] },
getTextureUV(id) { return textureAtlas.getTextureUV(id) },
@@ -120,20 +139,28 @@ Promise.all([
getBlockFlags(id) { return { opaque: false } },
getBlockProperties(id) { return null },
getDefaultBlockProperties(id) { return null },
+ getItemModel(id) { return itemModels[id.toString()] },
}
// === Item rendering ===
+ const context: ItemRenderingContext = {
+ "bundle/selected_item": 0
+ }
+
const itemCanvas = document.getElementById('item-display') as HTMLCanvasElement
const itemGl = itemCanvas.getContext('webgl')!
const itemInput = document.getElementById('item-input') as HTMLInputElement
itemInput.value = localStorage.getItem('deepslate_demo_item') ?? 'stone'
- const itemRenderer = new ItemRenderer(itemGl, Identifier.parse(itemInput.value), resources)
+ const itemStack = ItemStack.fromString(itemInput.value)
+ const itemRenderer = new ItemRenderer(itemGl, itemStack, resources, context)
itemInput.addEventListener('keyup', () => {
try {
const id = itemInput.value
- itemRenderer.setItem(new ItemStack(Identifier.parse(id), 1))
+ const itemStack = ItemStack.fromString(itemInput.value)
+ itemGl.clear(itemGl.DEPTH_BUFFER_BIT | itemGl.COLOR_BUFFER_BIT);
+ itemRenderer.setItem(itemStack, context)
itemRenderer.drawItem()
itemInput.classList.remove('invalid')
localStorage.setItem('deepslate_demo_item', id)
diff --git a/src/core/Effects.ts b/src/core/Effects.ts
index 7e968c2..9445804 100644
--- a/src/core/Effects.ts
+++ b/src/core/Effects.ts
@@ -1,7 +1,6 @@
import type { NbtCompound, NbtTag } from '../nbt/index.js'
import { NbtType } from '../nbt/index.js'
-import type { Color } from '../util/index.js'
-import { intToRgb } from '../util/index.js'
+import { Color } from '../util/index.js'
import { Identifier } from './Identifier.js'
export const EFFECT_COLORS = new Map([
@@ -139,7 +138,7 @@ export namespace PotionContents {
export function getColor(contents: PotionContents): Color {
if (contents.customColor) {
- return intToRgb(contents.customColor)
+ return Color.intToRgb(contents.customColor)
}
const effects = getAllEffects(contents)
return mixEffectColors(effects)
@@ -162,7 +161,7 @@ export namespace PotionContents {
for (const effect of effects) {
const color = EFFECT_COLORS.get(effect.effect.toString())
if (color === undefined) continue
- const rgb = intToRgb(color)
+ const rgb = Color.intToRgb(color)
const amplifier = effect.amplifier + 1
r += amplifier * rgb[0]
g += amplifier * rgb[1]
@@ -170,7 +169,7 @@ export namespace PotionContents {
total += amplifier
}
if (total === 0) {
- return intToRgb(-13083194)
+ return Color.intToRgb(-13083194)
}
r = r / total
g = g / total
diff --git a/src/core/Item.ts b/src/core/Item.ts
new file mode 100644
index 0000000..22d5f1f
--- /dev/null
+++ b/src/core/Item.ts
@@ -0,0 +1,31 @@
+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 2c99b36..8bd829c 100644
--- a/src/core/ItemStack.ts
+++ b/src/core/ItemStack.ts
@@ -1,30 +1,46 @@
+import { NbtParser } from '../nbt/NbtParser.js'
import type { NbtTag } from '../nbt/index.js'
import { NbtCompound, NbtInt, NbtString } from '../nbt/index.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
-
+
constructor(
- public id: Identifier,
+ public readonly id: Identifier,
public count: number,
- public components: Map = new Map(),
- ) {}
+ public readonly components: Map = new Map(),
+ ) {
+ this.item = Holder.reference(Item.REGISTRY, id, false)
+ }
- public getComponent(key: string | Identifier, reader: (tag: NbtTag) => T) {
+ public getComponent(key: string | Identifier, reader: (tag: NbtTag) => T, includeDefaultComponents: boolean = true): T | undefined {
if (typeof key === 'string') {
key = Identifier.parse(key)
}
+
+ if (this.components.has('!' + key.toString())){
+ return undefined
+ }
const value = this.components.get(key.toString())
if (value) {
return reader(value)
}
- return undefined
+ return includeDefaultComponents ? this.item.value()?.getComponent(key, reader) : undefined
}
- public hasComponent(key: string | Identifier) {
+ public hasComponent(key: string | Identifier, includeDefaultComponents: boolean = true): boolean {
if (typeof key === 'string') {
key = Identifier.parse(key)
}
- return this.components.has(key.toString())
+ if (this.components.has('!' + key.toString())){
+ return false
+ }
+
+ return this.components.has(key.toString()) || (includeDefaultComponents && (this.item.value()?.hasComponent(key) ?? false))
}
public clone(): ItemStack {
@@ -77,6 +93,53 @@ export class ItemStack {
return result
}
+ public static fromString(string: string) {
+ const reader = new StringReader(string)
+
+ while (reader.canRead() && reader.peek() !== '[') {
+ reader.skip()
+ }
+ const itemId = Identifier.parse(reader.getRead())
+ if (!reader.canRead()){
+ return new ItemStack(itemId, 1)
+ }
+
+ const components = new Map()
+ reader.skip()
+ if (reader.peek() === ']'){
+ return new ItemStack(itemId, 1, components)
+ }
+ do{
+ if (reader.peek() === '!'){
+ reader.skip()
+ const start = reader.cursor
+ while (reader.canRead() && reader.peek() !== ']' && reader.peek() !== ',') {
+ reader.skip()
+ }
+ components.set('!' + Identifier.parse(reader.getRead(start)).toString(), new NbtCompound())
+ } else {
+ const start = reader.cursor
+ while (reader.canRead() && reader.peek() !== '=') {
+ reader.skip()
+ }
+ const component = Identifier.parse(reader.getRead(start)).toString()
+ if (!reader.canRead()) break;
+ reader.skip()
+ const tag = NbtParser.readTag(reader)
+ components.set(component, tag)
+ }
+ if (!reader.canRead()) break;
+ if (reader.peek() === ']'){
+ return new ItemStack(itemId, 1, components)
+ }
+ if (reader.peek() !== ','){
+ throw new Error('Expected , or ]')
+ }
+ reader.skip()
+ } while (reader.canRead())
+ throw new Error('Missing closing ]')
+ }
+
public toNbt() {
const result = new NbtCompound()
.set('id', new NbtString(this.id.toString()))
diff --git a/src/core/index.ts b/src/core/index.ts
index bdb8c1f..fba2592 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -8,9 +8,11 @@ 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/nbt/tags/Util.ts b/src/nbt/tags/Util.ts
new file mode 100644
index 0000000..e5cf0ef
--- /dev/null
+++ b/src/nbt/tags/Util.ts
@@ -0,0 +1,23 @@
+import { NbtByte, NbtCompound, NbtDouble, NbtInt, NbtList, NbtString, NbtTag } from "./index.js"
+
+export function jsonToNbt(value: unknown): NbtTag {
+ if (typeof value === 'string') {
+ return new NbtString(value)
+ }
+ if (typeof value === 'number') {
+ return Number.isInteger(value) ? new NbtInt(value) : new NbtDouble(value)
+ }
+ if (typeof value === 'boolean') {
+ return new NbtByte(value)
+ }
+ if (Array.isArray(value)) {
+ return new NbtList(value.map(jsonToNbt))
+ }
+ if (typeof value === 'object' && value !== null) {
+ return new NbtCompound(
+ new Map(Object.entries(value ?? {})
+ .map(([k, v]) => [k, jsonToNbt(v)]))
+ )
+ }
+ return new NbtByte(0)
+}
diff --git a/src/nbt/tags/index.ts b/src/nbt/tags/index.ts
index a0ed412..5a09ad1 100644
--- a/src/nbt/tags/index.ts
+++ b/src/nbt/tags/index.ts
@@ -14,3 +14,5 @@ export * from './NbtShort.js'
export * from './NbtString.js'
export * from './NbtTag.js'
export * from './NbtType.js'
+export * from './Util.js'
+
diff --git a/src/render/BlockColors.ts b/src/render/BlockColors.ts
index 7787dd6..2290cd3 100644
--- a/src/render/BlockColors.ts
+++ b/src/render/BlockColors.ts
@@ -1,14 +1,13 @@
import { clamp } from '../math/index.js'
-import type { Color } from '../util/index.js'
-import { intToRgb } from '../util/index.js'
+import { Color } from '../util/index.js'
const grass: Color = [124 / 255, 189 / 255, 107 / 255]
-const spruce = intToRgb(6396257)
-const birch = intToRgb(8431445)
-const foliage = intToRgb(4764952)
-const water = intToRgb(4159204)
-const attached_stem = intToRgb(8431445)
-const lily_pad = intToRgb(2129968)
+const spruce = Color.intToRgb(6396257)
+const birch = Color.intToRgb(8431445)
+const foliage = Color.intToRgb(4764952)
+const water = Color.intToRgb(4159204)
+const attached_stem = Color.intToRgb(8431445)
+const lily_pad = Color.intToRgb(2129968)
const redstone = (power: number): Color => {
const a = power / 15
diff --git a/src/render/BlockModel.ts b/src/render/BlockModel.ts
index 8ae5f50..672badc 100644
--- a/src/render/BlockModel.ts
+++ b/src/render/BlockModel.ts
@@ -11,7 +11,7 @@ import type { TextureAtlasProvider, UV } from './TextureAtlas.js'
type Axis = 'x' | 'y' | 'z'
-type Display = 'thirdperson_righthand' | 'thirdperson_lefthand' | 'firstperson_righthand' | 'firstperson_lefthand' | 'gui' | 'head' | 'ground' | 'fixed'
+export type Display = 'thirdperson_righthand' | 'thirdperson_lefthand' | 'firstperson_righthand' | 'firstperson_lefthand' | 'gui' | 'head' | 'ground' | 'fixed' | 'none'
type BlockModelFace = {
texture: string,
diff --git a/src/render/ItemColors.ts b/src/render/ItemColors.ts
deleted file mode 100644
index fee65e7..0000000
--- a/src/render/ItemColors.ts
+++ /dev/null
@@ -1,210 +0,0 @@
-import type { ItemStack } from '../core/index.js'
-import { Identifier, PotionContents } from '../core/index.js'
-import { NbtIntArray } from '../index.js'
-import type { Color } from '../util/index.js'
-import { intToRgb } from '../util/index.js'
-import { BlockColors } from './BlockColors.js'
-
-type Tint = Color | ((index: number) => Color)
-const ItemColors = new Map Tint>()
-
-export function getItemColor(item: ItemStack): Tint {
- if (item.id.namespace !== Identifier.DEFAULT_NAMESPACE) return [1, 1, 1]
- const tint = ItemColors.get(item.id.path)
- return tint ? tint(item) : [1, 1, 1]
-}
-
-function register(items: string[], fn: (item: ItemStack) => Tint) {
- for (const item of items) {
- ItemColors.set(item, fn)
- }
-}
-
-function getDyedColor(item: ItemStack, fallback: number) {
- const dyedColor = item.getComponent('dyed_color', tag => {
- return tag.isCompound() ? tag.getNumber('rgb') : tag.getAsNumber()
- })
- return intToRgb(dyedColor ?? fallback)
-}
-
-register([
- 'leather_helmet',
- 'leather_chestplate',
- 'leather_leggings',
- 'leather_boots',
- 'leather_horse_armor',
-], item => {
- const color = getDyedColor(item, -6265536)
- return (index: number) => index > 0 ? [1, 1, 1] : color
-})
-
-register([
- 'wolf_armor',
-], item => {
- const color = getDyedColor(item, 0)
- return (index: number) => index !== 1 ? [1, 1, 1] : color
-})
-
-register([
- 'tall_grass',
- 'large_fern',
-], () => [124 / 255, 189 / 255, 107 / 255])
-
-register([
- 'firework_star',
-], item => {
- const colors = item.getComponent('firework_explosion', tag => {
- return tag.isCompound() ? tag.getIntArray('colors') : new NbtIntArray()
- })
- const color: Color = (() => {
- if (!colors || colors.length === 0) {
- return intToRgb(9079434)
- }
- if (colors.length === 1) {
- return intToRgb(colors.get(0)!.getAsNumber())
- }
- let [r, g, b] = [0, 0, 0]
- for (const color of colors.getItems()) {
- r += (color.getAsNumber() & 0xFF0000) >> 16
- g += (color.getAsNumber() & 0xFF00) >> 8
- b += (color.getAsNumber() & 0xFF) >> 0
- }
- r /= colors.length
- g /= colors.length
- b /= colors.length
- return [r, g, b]
- })()
- return (index: number) => index !== 1 ? [1, 1, 1] : color
-})
-
-register([
- 'potion',
- 'splash_potion',
- 'lingering_potion',
- 'tipped_arrow',
-], item => {
- const data = item.getComponent('potion_contents', PotionContents.fromNbt)
- if (!data) {
- return () => [1, 1, 1]
- }
- const color = PotionContents.getColor(data)
- return (index: number) => index > 0 ? [1, 1, 1] : color
-})
-
-const SpawnEggs: [string, number, number][] = [
- ['allay', 56063, 44543],
- ['armadillo', 11366765, 8538184],
- ['axolotl', 16499171, 10890612],
- ['bat', 4996656, 986895],
- ['bee', 15582019, 4400155],
- ['blaze', 16167425, 16775294],
- ['bogged', 9084018, 3231003],
- ['breeze', 11506911, 9529055],
- ['cat', 15714446, 9794134],
- ['camel', 16565097, 13341495],
- ['cave_spider', 803406, 11013646],
- ['chicken', 10592673, 16711680],
- ['cod', 12691306, 15058059],
- ['cow', 4470310, 10592673],
- ['creeper', 894731, 0],
- ['dolphin', 2243405, 16382457],
- ['donkey', 5457209, 8811878],
- ['drowned', 9433559, 7969893],
- ['elder_guardian', 13552826, 7632531],
- ['ender_dragon', 1842204, 14711290],
- ['enderman', 1447446, 0],
- ['endermite', 1447446, 7237230],
- ['evoker', 9804699, 1973274],
- ['fox', 14005919, 13396256],
- ['frog', 13661252, 16762748],
- ['ghast', 16382457, 12369084],
- ['glow_squid', 611926, 8778172],
- ['goat', 10851452, 5589310],
- ['guardian', 5931634, 15826224],
- ['hoglin', 13004373, 6251620],
- ['horse', 12623485, 15656192],
- ['husk', 7958625, 15125652],
- ['iron_golem', 14405058, 7643954],
- ['llama', 12623485, 10051392],
- ['magma_cube', 3407872, 16579584],
- ['mooshroom', 10489616, 12040119],
- ['mule', 1769984, 5321501],
- ['ocelot', 15720061, 5653556],
- ['panda', 15198183, 1776418],
- ['parrot', 894731, 16711680],
- ['phantom', 4411786, 8978176],
- ['pig', 15771042, 14377823],
- ['piglin', 10051392, 16380836],
- ['piglin_brute', 5843472, 16380836],
- ['pillager', 5451574, 9804699],
- ['polar_bear', 15658718, 14014157],
- ['pufferfish', 16167425, 3654642],
- ['rabbit', 10051392, 7555121],
- ['ravager', 7697520, 5984329],
- ['salmon', 10489616, 951412],
- ['sheep', 15198183, 16758197],
- ['shulker', 9725844, 5060690],
- ['silverfish', 7237230, 3158064],
- ['skeleton', 12698049, 4802889],
- ['skeleton_horse', 6842447, 15066584],
- ['slime', 5349438, 8306542],
- ['sniffer', 8855049, 2468720],
- ['snow_golem', 14283506, 8496292],
- ['spider', 3419431, 11013646],
- ['squid', 2243405, 7375001],
- ['stray', 6387319, 14543594],
- ['strider', 10236982, 5065037],
- ['tadpole', 7164733, 1444352],
- ['trader_llama', 15377456, 4547222],
- ['tropical_fish', 15690005, 16775663],
- ['turtle', 15198183, 44975],
- ['vex', 8032420, 15265265],
- ['villager', 5651507, 12422002],
- ['vindicator', 9804699, 2580065],
- ['wandering_trader', 4547222, 15377456],
- ['warden', 1001033, 3790560],
- ['witch', 3407872, 5349438],
- ['wither', 1315860, 5075616],
- ['wither_skeleton', 1315860, 4672845],
- ['wolf', 14144467, 13545366],
- ['zoglin', 13004373, 15132390],
- ['zombie', 44975, 7969893],
- ['zombie_horse', 3232308, 9945732],
- ['zombie_villager', 5651507, 7969893],
- ['zombified_piglin', 15373203, 5009705],
-]
-
-for (const egg of SpawnEggs) {
- register([`${egg[0]}_spawn_egg`], () => {
- return (index: number) => intToRgb(index === 0 ? egg[1] : egg[2])
- })
-}
-
-for (const id of [
- 'grass_block',
- 'short_grass',
- 'fern',
- 'vine',
- 'oak_leaves',
- 'spruce_leaves',
- 'birch_leaves',
- 'jungle_leaves',
- 'acacia_leaves',
- 'dark_oak_leaves',
- 'lily_pad',
-]) {
- const color = BlockColors[id]({})
- register([id], () => color)
-}
-
-register([
- 'mangrove_leaves',
-], () => intToRgb(9619016))
-
-register([
- 'filled_map',
-], item => {
- const mapColor = item.getComponent('map_color', tag => tag.getAsNumber())
- const color = intToRgb(mapColor ?? 4603950)
- return (index: number) => index === 0 ? [1, 1, 1] : color
-})
diff --git a/src/render/ItemModel.ts b/src/render/ItemModel.ts
new file mode 100644
index 0000000..7dde809
--- /dev/null
+++ b/src/render/ItemModel.ts
@@ -0,0 +1,392 @@
+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"
+
+export interface ItemModelProvider {
+ getItemModel(id: Identifier): ItemModel | null
+}
+
+export abstract class ItemModel {
+
+ public abstract getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh
+
+}
+
+const MISSING_MESH: Mesh = new Mesh() ///TODO
+
+export namespace ItemModel {
+ export function fromJson(obj: unknown): ItemModel {
+ const root = Json.readObject(obj) ?? {}
+ const type = Json.readString(root.type)?.replace(/^minecraft:/, '')
+ switch (type) {
+ case 'empty': return new Empty()
+ case 'model': return new Model(
+ Identifier.parse(Json.readString(root.model) ?? ''),
+ Json.readArray(root.tints, ItemTint.fromJson) ?? []
+ )
+ case 'composite': return new Composite(
+ Json.readArray(root.models, ItemModel.fromJson) ?? []
+ )
+ case 'condition': return new Condition(
+ Condition.propertyFromJson(root),
+ ItemModel.fromJson(root.on_true),
+ ItemModel.fromJson(root.on_false)
+ )
+ case 'select': return new Select(
+ Select.propertyFromJson(root),
+ new Map(Json.readArray(root.cases, caseObj => {
+ const caseRoot = Json.readObject(caseObj) ?? {}
+ return [Json.readString(caseRoot.when) ?? '', ItemModel.fromJson(caseRoot.model)]
+ })),
+ root.fallback ? ItemModel.fromJson(root.fallback) : undefined
+ )
+ case 'range_dispatch': return new RangeDispatch(
+ RangeDispatch.propertyFromJson(root),
+ Json.readNumber(root.scale) ?? 1,
+ Json.readArray(root.entries, entryObj => {
+ const entryRoot = Json.readObject(entryObj) ?? {}
+ return {threshold: Json.readNumber(entryRoot.threshold) ?? 0, model: ItemModel.fromJson(entryRoot.model)}
+ }) ?? [],
+ root.fallback ? ItemModel.fromJson(root.fallback) : undefined
+ )
+ case 'special': return new Special(
+ SpecialModel.fromJson(root.model),
+ Identifier.parse(Json.readString(root.base) ?? '')
+ )
+ case 'bundle/selected_item': return new BundleSelectedItem()
+ default:
+ throw new Error(`Invalid item model type ${type}`)
+ }
+ }
+
+ export class Empty extends ItemModel {
+ public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh{
+ return new Mesh()
+ }
+ }
+
+ export class Model extends ItemModel {
+ constructor(
+ private modelId: Identifier,
+ private tints: ItemTint[]
+ ) {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh{
+ const model = resources.getBlockModel(this.modelId)
+ if (!model) {
+ throw new Error(`Model ${this.modelId} does not exist (trying to render ${item.toString()})`)
+ }
+
+ let tint = (i: number): Color => {
+ if (i < this.tints.length) {
+ return this.tints[i].getTint(item, context)
+ } else {
+ return [1, 1, 1]
+ }
+ }
+
+ const mesh = model.getMesh(resources, Cull.none(), tint)
+ mesh.transform(model.getDisplayTransform(context.display_context ?? 'gui'))
+ return mesh
+ }
+
+ }
+
+ export class Composite extends ItemModel {
+ constructor(
+ private models: ItemModel[]
+ ) {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh {
+ const mesh = new Mesh()
+ this.models.forEach(model => mesh.merge(model.getMesh(item, resources, context)))
+ return mesh
+ }
+ }
+
+ export class Condition extends ItemModel {
+ constructor(
+ private property: (item: ItemStack, context: ItemRenderingContext) => boolean,
+ private onTrue: ItemModel,
+ private onFalse: ItemModel
+ ) {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh {
+ return (this.property(item, context) ? this.onTrue : this.onFalse).getMesh(item, resources, context)
+ }
+
+ static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => boolean{
+ const property = Json.readString(root.property)?.replace(/^minecraft:/, '')
+
+ switch (property){
+ case 'fishing_rod/cast':
+ case 'selected':
+ case 'carried':
+ case 'extended_view':
+ return (item, context) => context[property] ?? false
+ case 'view_entity':
+ return (item, context) => context.context_entity_is_view_entity ?? false
+ case 'using_item':
+ return (item, context) => (context.use_duration ?? -1) >= 0
+ 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())
+ 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)
+ }
+ 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)
+ 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
+ const flag = tag.getList('flags').getNumber(index)
+ return flag !== undefined && flag !== 0
+ }) ?? false
+ default:
+ throw new Error(`Invalid condition property ${property}`)
+ }
+ }
+ }
+
+ export class Select extends ItemModel {
+ constructor(
+ private property: (item: ItemStack, context: ItemRenderingContext) => string | null,
+ private cases: Map,
+ private fallback?: ItemModel
+ ) {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh {
+ const value = this.property(item, context)
+ return ((value !== null ? this.cases.get(value) : undefined) ?? this.fallback)?.getMesh(item, resources, context) ?? MISSING_MESH
+ }
+
+ static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => string | null{
+ const property = Json.readString(root.property)?.replace(/^minecraft:/, '')
+
+ switch (property){
+ case 'main_hand':
+ return (item, context) => context.main_hand ?? 'right'
+ case 'display_context':
+ return (item, context) => context.display_context ?? 'gui'
+ case 'context_dimension':
+ 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 '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 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 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
+ const list = tag.getList('strings')
+ if (list.length <= index) return undefined
+ return list.getString(index)
+ }) ?? null
+ default:
+ throw new Error(`Invalid select property ${property}`)
+
+ }
+ }
+ }
+
+ export class RangeDispatch extends ItemModel {
+ private entries: {threshold: number, model: ItemModel}[]
+
+ constructor(
+ private property: (item: ItemStack, context: ItemRenderingContext) => number,
+ private scale: number,
+ entries: {threshold: number, model: ItemModel}[],
+ private fallback?: ItemModel
+ ) {
+ super()
+ this.entries = entries.sort((a, b) => a.threshold - b.threshold)
+ }
+
+ public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh {
+ const value = this.property(item, context) * this.scale
+ let model = this.fallback
+ for (const entry of this.entries) {
+ if (entry.threshold <= value) {
+ model = entry.model
+ } else {
+ break
+ }
+ }
+ return model?.getMesh(item, resources, context) ?? MISSING_MESH
+ }
+
+ 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)
+ }, 0)
+ }
+
+ return (item, context) => calculateBundleWeight(item)
+ 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)
+ }
+ }
+ 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)
+ }
+ }
+ 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
+
+ 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()
+ }
+ case 'compass': return (item, context) => context.compass_angle ?? 0 // TODO: calculate properly?
+ case 'crossbow/pull': return (item, context) => context['crossbow/pull'] ?? 0
+ case 'use_duration':
+ const remaining = Json.readBoolean(root.remaining) ?? true
+ return (item, context) => {
+ if (context.use_duration === undefined || context.use_duration < 0) return 0
+ if (remaining) return Math.max((context.max_use_duration ?? 0) - (context.use_duration), 0)
+ return context.use_duration
+ }
+ case 'use_cycle':
+ const period = Json.readNumber(root.period) ?? 1
+ return (item, context) => {
+ if (context.use_duration === undefined || context.use_duration < 0) return 0
+ return Math.max((context.max_use_duration ?? 0) - (context.use_duration ?? 0), 0) % period
+ }
+ 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 tag.getList('floats').getNumber(index)
+ }) ?? 0
+ default:
+ throw new Error(`Invalid select property ${property}`)
+ }
+ }
+ }
+
+ export class Special extends ItemModel {
+ constructor(
+ private specialModel: SpecialModel,
+ private base: Identifier
+ ) {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh {
+ const mesh = this.specialModel.getMesh(item, resources)
+ const model = resources.getBlockModel(this.base)
+ if (!model) {
+ throw new Error(`Base model ${this.base} does not exist (trying to render ${item.toString()})`)
+ }
+ mesh.transform(model.getDisplayTransform(context.display_context ?? 'gui'))
+ return mesh
+ }
+ }
+
+ export class BundleSelectedItem extends 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()
+ }
+ }
+}
+
diff --git a/src/render/ItemRenderer.ts b/src/render/ItemRenderer.ts
index ade44c5..00594af 100644
--- a/src/render/ItemRenderer.ts
+++ b/src/render/ItemRenderer.ts
@@ -1,60 +1,82 @@
import { mat4 } from 'gl-matrix'
-import { Identifier } from '../core/index.js'
import { ItemStack } from '../core/ItemStack.js'
-import { Cull, SpecialRenderers, type Color } from '../index.js'
-import type { BlockModelProvider } from './BlockModel.js'
-import { getItemColor } from './ItemColors.js'
-import type { Mesh } from './Mesh.js'
+import { Identifier } from '../core/index.js'
+import { Color } from '../index.js'
+import type { BlockModelProvider, Display } from './BlockModel.js'
+import { ItemModelProvider } from './ItemModel.js'
+import { Mesh } from './Mesh.js'
import { Renderer } from './Renderer.js'
import type { TextureAtlasProvider } from './TextureAtlas.js'
-interface ModelRendererOptions {
- /** Force the tint index of the item */
- tint?: Color,
-}
+export interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider, ItemModelProvider {}
+
+export type ItemRenderingContext = {
+ display_context?: Display,
-interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider {}
+ 'fishing_rod/cast'?: boolean,
+ 'bundle/selected_item'?: number,
+ selected?: boolean,
+ carried?: boolean,
+ extended_view?: 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
+
+ cooldown_percentage?: {[key: string]: number},
+ game_time?: number,
+ compass_angle?: number,
+ use_duration?: number,
+ max_use_duration?: number,
+ 'crossbow/pull'?: number
+}
export class ItemRenderer extends Renderer {
- private item: ItemStack
- private mesh: Mesh
- private readonly tint: Color | ((index: number) => Color) | undefined
+ private mesh!: Mesh
private readonly atlasTexture: WebGLTexture
+
constructor(
gl: WebGLRenderingContext,
- item: Identifier | ItemStack,
+ private item: ItemStack,
private readonly resources: ItemRendererResources,
- options?: ModelRendererOptions,
+ context: ItemRenderingContext = {},
) {
super(gl)
- this.item = item instanceof ItemStack ? item : new ItemStack(item, 1)
- this.mesh = this.getItemMesh()
- this.tint = options?.tint
+ this.updateMesh(context)
this.atlasTexture = this.createAtlasTexture(this.resources.getTextureAtlas())
}
- public setItem(item: Identifier | ItemStack) {
- this.item = item instanceof ItemStack ? item : new ItemStack(item, 1)
- this.mesh = this.getItemMesh()
+ public setItem(item: ItemStack, context: ItemRenderingContext = {}) {
+ this.item = item
+ this.updateMesh(context)
+ }
+
+ public updateMesh(context: ItemRenderingContext = {}) {
+ this.mesh = ItemRenderer.getItemMesh(this.item, this.resources, context)
+ this.mesh.computeNormals()
+ this.mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true })
}
- private getItemMesh() {
- const model = this.resources.getBlockModel(this.item.id.withPrefix('item/'))
- if (!model) {
- throw new Error(`Item model for ${this.item.toString()} does not exist`)
+ public static getItemMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext) {
+ const itemModelId = item.getComponent('item_model', tag => tag.getAsString())
+ if (itemModelId === undefined){
+ return new Mesh()
}
- let tint = this.tint
- if (!tint && this.item.id.namespace === Identifier.DEFAULT_NAMESPACE) {
- tint = getItemColor(this.item)
+
+ const itemModel = resources.getItemModel(Identifier.parse(itemModelId))
+ if (!itemModel) {
+ throw new Error(`Item model ${itemModelId} does not exist (defined by item ${item.toString()})`)
}
- const mesh = model.getMesh(this.resources, Cull.none(), tint)
- const specialMesh = SpecialRenderers.getItemMesh(this.item, this.resources)
- mesh.merge(specialMesh)
- mesh.transform(model.getDisplayTransform('gui'))
- mesh.computeNormals()
- mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true })
+
+ const mesh = itemModel.getMesh(item, resources, context)
+
return mesh
+
}
protected override getPerspective() {
diff --git a/src/render/ItemTint.ts b/src/render/ItemTint.ts
new file mode 100644
index 0000000..41c5504
--- /dev/null
+++ b/src/render/ItemTint.ts
@@ -0,0 +1,179 @@
+import { Color, ItemRenderingContext, ItemStack, Json, NbtIntArray, PotionContents } from "../index.js"
+
+export abstract class ItemTint {
+ public abstract getTint(item: ItemStack, context: ItemRenderingContext): Color
+}
+
+const INVALID_COLOR: Color = [0, 0, 0]
+
+export namespace ItemTint {
+ export function fromJson(obj: unknown): ItemTint {
+ const root = Json.readObject(obj) ?? {}
+ const type = Json.readString(root.type)?.replace(/^minecraft:/, '')
+ switch (type) {
+ case 'constant': return new Constant(
+ Color.fromJson(root.value) ?? INVALID_COLOR
+ )
+ case 'dye': return new Dye(
+ Color.fromJson(root.default) ?? INVALID_COLOR
+ )
+ case 'grass': return new Grass(
+ Json.readNumber(root.temperature) ?? 0,
+ Json.readNumber(root.downfall) ?? 0
+ )
+ case 'firework': return new Firework(
+ Color.fromJson(root.default) ?? INVALID_COLOR
+ )
+ case 'potion': return new Potion(
+ Color.fromJson(root.default) ?? INVALID_COLOR
+ )
+ case 'map_color': return new MapColor(
+ Color.fromJson(root.default) ?? INVALID_COLOR
+ )
+ case 'custom_model_data': return new CustomModelData(
+ Json.readInt(root.index) ?? 0,
+ Color.fromJson(root.default) ?? INVALID_COLOR
+ )
+ case 'team': return new Team(
+ Color.fromJson(root.default) ?? INVALID_COLOR
+ )
+ default:
+ throw new Error(`Invalid item tint type ${type}`)
+ }
+ }
+
+ export class Constant extends ItemTint{
+ constructor(
+ public value: Color
+ ) {
+ super()
+ }
+
+ public getTint(item: ItemStack): Color {
+ return this.value
+ }
+ }
+
+ export class Dye extends ItemTint{
+ constructor(
+ public default_color: Color
+ ) {
+ super()
+ }
+
+ 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)
+ }
+ }
+
+ export class Grass extends ItemTint{
+ constructor(
+ public temperature: number,
+ public downfall: number
+ ) {
+ super()
+ }
+
+ public getTint(item: ItemStack): Color {
+ return [124 / 255, 189 / 255, 107 / 255] // TODO: this is hardcoded to the same value as for blocks
+ }
+ }
+
+ export class Firework extends ItemTint{
+ constructor(
+ public default_color: Color
+ ) {
+ super()
+ }
+
+ 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 color: Color = (() => {
+ if (!colors || colors.length === 0) {
+ return this.default_color
+ }
+ if (colors.length === 1) {
+ return Color.intToRgb(colors.get(0)!.getAsNumber())
+ }
+ let [r, g, b] = [0, 0, 0]
+ for (const color of colors.getItems()) {
+ r += (color.getAsNumber() & 0xFF0000) >> 16
+ g += (color.getAsNumber() & 0xFF00) >> 8
+ b += (color.getAsNumber() & 0xFF) >> 0
+ }
+ r /= colors.length
+ g /= colors.length
+ b /= colors.length
+ return [r / 255, g / 255, b / 255]
+ })()
+ return color
+ }
+ }
+
+ export class Potion extends ItemTint {
+ constructor(
+ public default_color: Color
+ ) {
+ super()
+ }
+
+ 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)
+ }
+ }
+
+ export class MapColor extends ItemTint {
+ constructor(
+ public default_color: Color
+ ) {
+ super()
+ }
+
+ 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)
+ }
+ }
+
+ export class CustomModelData extends ItemTint {
+ constructor(
+ public index: number,
+ public default_color: Color
+ ) {
+ super()
+ }
+
+ 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
+ }
+ }
+
+ export class Team extends ItemTint {
+ constructor(
+ public default_color: Color
+ ) {
+ super()
+ }
+
+ public getTint(item: ItemStack, context: ItemRenderingContext): Color {
+ return context.context_entity_team_color ?? this.default_color
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/render/SpecialModel.ts b/src/render/SpecialModel.ts
new file mode 100644
index 0000000..123fed1
--- /dev/null
+++ b/src/render/SpecialModel.ts
@@ -0,0 +1,230 @@
+import { mat4 } from "gl-matrix"
+import { Direction, Identifier, ItemStack, Json, SpecialRenderers, TextureAtlasProvider } from "../index.js"
+import { Mesh } from "./Mesh.js"
+
+
+
+export abstract class SpecialModel {
+ public abstract getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh
+}
+
+export namespace SpecialModel {
+ export function fromJson(obj: unknown) {
+ const root = Json.readObject(obj) ?? {}
+ const type = Json.readString(root.type)?.replace(/^minecraft:/, '')
+ switch (type) {
+ case 'bed': return new Bed(
+ Identifier.parse(Json.readString(root.texture) ?? '')
+ )
+ case 'banner': return new Banner(
+ Json.readString(root.color) ?? ''
+ )
+ case 'conduit': return new Conduit()
+ case 'chest': return new Chest(
+ Identifier.parse(Json.readString(root.texture) ?? ''),
+ Json.readNumber(root.openness) ?? 0
+ )
+ case 'head': return new Head(
+ Json.readString(root.kind) ?? '',
+ typeof root.texture === 'string' ? Identifier.parse(root.texture) : undefined,
+ Json.readNumber(root.animation) ?? 0
+ )
+ case 'shulker_box': return new ShulkerBox(
+ Identifier.parse(Json.readString(root.texture) ?? ''),
+ Json.readNumber(root.openness) ?? 0,
+ (Json.readString(root.orientation) ?? 'up') as Direction
+ )
+ case 'shield': return new Shield()
+ case 'trident': return new Trident()
+ case 'decorated_pot': return new DecoratedPot()
+ case 'standing_sign': return new StandingSign(
+ Json.readString(root.wood_type) ?? '',
+ typeof root.texture === 'string' ? Identifier.parse(root.texture) : undefined
+ )
+ case 'hanging_sign': return new HangingSign(
+ Json.readString(root.wood_type) ?? '',
+ typeof root.texture === 'string' ? Identifier.parse(root.texture) : undefined
+ )
+ default:
+ throw new Error(`Invalid item model type ${type}`)
+ }
+ }
+
+ class Bed extends SpecialModel {
+ private readonly renderer
+
+ constructor(
+ texture: Identifier
+ ) {
+ super()
+ this.renderer = SpecialRenderers.bedRenderer(texture)
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ const headMesh = this.renderer("head", resources)
+ const footMesh = this.renderer("foot", resources)
+ const t = mat4.create()
+ mat4.translate(t, t, [0, 0, -16])
+ return headMesh.merge(footMesh.transform(t))
+ }
+ }
+
+ class Banner extends SpecialModel {
+ private readonly renderer
+
+ constructor(
+ color: string
+ ) {
+ super()
+ this.renderer = SpecialRenderers.bannerRenderer(color)
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return this.renderer(resources)
+ }
+ }
+
+ class Conduit extends SpecialModel {
+ constructor() {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return SpecialRenderers.conduitRenderer(resources)
+ }
+ }
+
+ class Chest extends SpecialModel {
+ private readonly renderer
+
+ constructor(
+ texture: Identifier,
+ openness: number
+ ) {
+ super()
+ this.renderer = SpecialRenderers.chestRenderer(texture)
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ const t = mat4.create()
+ mat4.translate(t, t, [8, 8, 8])
+ mat4.rotateY(t, t, Math.PI)
+ mat4.translate(t, t, [-8, -8, -8])
+ return this.renderer(resources).transform(t)
+ }
+ }
+
+ class Head extends SpecialModel {
+ private readonly renderer
+
+ constructor(
+ kind: string,
+ texture: Identifier | undefined,
+ animation: number
+ ) {
+ super()
+
+ this.renderer = ({
+ 'skeleton': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('skeleton/skeleton'), 2),
+ 'wither_skeleton': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('skeleton/wither_skeleton'), 2),
+ 'zombie': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('zombie/zombie'), 1),
+ 'creeper': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('creeper/creeper'), 2),
+ 'dragon': () => SpecialRenderers.dragonHeadRenderer(texture),
+ 'piglin': () => SpecialRenderers.piglinHeadRenderer(texture),
+ 'player': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('player/wide/steve'), 1), // TODO: fix texture
+ }[kind] ?? (() => () => new Mesh()))()
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return this.renderer(resources)
+ }
+ }
+
+ class ShulkerBox extends SpecialModel {
+ private readonly renderer
+
+ constructor(
+ texture: Identifier,
+ openness: number,
+ orientation: Direction
+ ) {
+ super()
+
+ this.renderer = SpecialRenderers.shulkerBoxRenderer(texture)
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return this.renderer(resources)
+ }
+ }
+
+ class Shield extends SpecialModel {
+ constructor() {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ const shieldMesh = SpecialRenderers.shieldRenderer(resources)
+ const t = mat4.create()
+ mat4.translate(t, t, [-3, 1, 0])
+ mat4.rotateX(t, t, -10 * Math.PI/180)
+ mat4.rotateY(t, t, -10 * Math.PI/180)
+ mat4.rotateZ(t, t, -5 * Math.PI/180)
+ return shieldMesh.transform(t)
+ }
+ }
+
+ class Trident extends SpecialModel {
+ constructor() {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return new Mesh() // TODO
+ }
+ }
+
+ class DecoratedPot extends SpecialModel {
+ constructor() {
+ super()
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return SpecialRenderers.decoratedPotRenderer(resources)
+ }
+ }
+
+ class StandingSign extends SpecialModel {
+ private readonly renderer
+
+ constructor(
+ wood_type: string,
+ texture?: Identifier
+ ) {
+ super()
+
+ this.renderer = SpecialRenderers.signRenderer(texture ?? Identifier.create(wood_type))
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return this.renderer(resources)
+ }
+ }
+
+ class HangingSign extends SpecialModel {
+ private readonly renderer
+
+ constructor(
+ wood_type: string,
+ texture?: Identifier
+ ) {
+ super()
+
+ this.renderer = SpecialRenderers.hangingSignRenderer(texture ?? Identifier.create(wood_type))
+ }
+
+ public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh {
+ return this.renderer(false, resources)
+ }
+ }
+}
diff --git a/src/render/SpecialRenderer.ts b/src/render/SpecialRenderer.ts
index 8723161..3ae9c33 100644
--- a/src/render/SpecialRenderer.ts
+++ b/src/render/SpecialRenderer.ts
@@ -1,6 +1,5 @@
import { mat4 } from 'gl-matrix'
-import type { ItemStack } from '../core/index.js'
-import { BlockState, Direction } from '../core/index.js'
+import { BlockState, Direction, Identifier } from '../core/index.js'
import { BlockColors } from './BlockColors.js'
import { BlockModel } from './BlockModel.js'
import { Cull } from './Cull.js'
@@ -26,371 +25,402 @@ function liquidRenderer(type: string, level: number, atlas: TextureAtlasProvider
}]).getMesh(atlas, cull, BlockColors[type]?.({}))
}
-function chestRenderer(type: string) {
- return (atlas: TextureAtlasProvider) => {
+
+export namespace SpecialRenderers {
+ export function chestRenderer(texture: Identifier) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/chest/').toString(),
+ }, [
+ {
+ from: [1, 0, 1],
+ to: [15, 10, 15],
+ faces: {
+ north: {uv: [10.5, 8.25, 14, 10.75], rotation: 180, texture: '#0'},
+ east: {uv: [7, 8.25, 10.5, 10.75], rotation: 180, texture: '#0'},
+ south: {uv: [3.5, 8.25, 7, 10.75], rotation: 180, texture: '#0'},
+ west: {uv: [0, 8.25, 3.5, 10.75], rotation: 180, texture: '#0'},
+ up: {uv: [7, 4.75, 10.5, 8.25], texture: '#0'},
+ down: {uv: [3.5, 4.75, 7, 8.25], texture: '#0'},
+ },
+ },
+ {
+ from: [1, 10, 1],
+ to: [15, 14, 15],
+ faces: {
+ north: {uv: [10.5, 3.75, 14, 4.75], rotation: 180, texture: '#0'},
+ east: {uv: [7, 3.75, 10.5, 4.75], rotation: 180, texture: '#0'},
+ south: {uv: [3.5, 3.75, 7, 4.75], rotation: 180, texture: '#0'},
+ west: {uv: [0, 3.75, 3.5, 4.75], rotation: 180, texture: '#0'},
+ up: {uv: [7, 0, 10.5, 3.5], texture: '#0'},
+ down: {uv: [3.5, 0, 7, 3.5], texture: '#0'},
+ },
+ },
+ {
+ from: [7, 7, 0],
+ to: [9, 11, 2],
+ faces: {
+ north: {uv: [0.25, 0.25, 0.75, 1.25], rotation: 180, texture: '#0'},
+ east: {uv: [0, 0.25, 0.25, 1.25], rotation: 180, texture: '#0'},
+ south: {uv: [1, 0.25, 1.5, 1.25], rotation: 180, texture: '#0'},
+ west: {uv: [0.75, 0.25, 1, 1.25], rotation: 180, texture: '#0'},
+ up: {uv: [0.25, 0, 0.75, 0.25], rotation: 180, texture: '#0'},
+ down: {uv: [0.75, 0, 1.25, 0.25], rotation: 180, texture: '#0'},
+ },
+ },
+ ]).getMesh(atlas, Cull.none())
+ }
+ }
+
+ export function decoratedPotRenderer(atlas: TextureAtlasProvider) {
return new BlockModel(undefined, {
- 0: `entity/chest/${type}`,
+ 0: 'entity/decorated_pot/decorated_pot_side',
+ 1: 'entity/decorated_pot/decorated_pot_base',
}, [
{
from: [1, 0, 1],
- to: [15, 10, 15],
+ to: [15, 16, 15],
faces: {
- north: {uv: [10.5, 8.25, 14, 10.75], rotation: 180, texture: '#0'},
- east: {uv: [7, 8.25, 10.5, 10.75], rotation: 180, texture: '#0'},
- south: {uv: [3.5, 8.25, 7, 10.75], rotation: 180, texture: '#0'},
- west: {uv: [0, 8.25, 3.5, 10.75], rotation: 180, texture: '#0'},
- up: {uv: [7, 4.75, 10.5, 8.25], texture: '#0'},
- down: {uv: [3.5, 4.75, 7, 8.25], texture: '#0'},
+ north: {uv: [1, 0, 15, 16], texture: '#0'},
+ east: {uv: [1, 0, 15, 16], texture: '#0'},
+ south: {uv: [1, 0, 15, 16], texture: '#0'},
+ west: {uv: [1, 0, 15, 16], texture: '#0'},
+ up: {uv: [0, 6.5, 7, 13.5], texture: '#1'},
+ down: {uv: [7, 6.5, 14, 13.5], texture: '#1'},
},
},
{
- from: [1, 10, 1],
- to: [15, 14, 15],
+ from: [5, 16, 5],
+ to: [11, 17, 11],
faces: {
- north: {uv: [10.5, 3.75, 14, 4.75], rotation: 180, texture: '#0'},
- east: {uv: [7, 3.75, 10.5, 4.75], rotation: 180, texture: '#0'},
- south: {uv: [3.5, 3.75, 7, 4.75], rotation: 180, texture: '#0'},
- west: {uv: [0, 3.75, 3.5, 4.75], rotation: 180, texture: '#0'},
- up: {uv: [7, 0, 10.5, 3.5], texture: '#0'},
- down: {uv: [3.5, 0, 7, 3.5], texture: '#0'},
+ north: {uv: [0, 5.5, 3, 6], texture: '#1'},
+ east: {uv: [3, 5.5, 6, 6], texture: '#1'},
+ south: {uv: [6, 5.5, 9, 6], texture: '#1'},
+ west: {uv: [9, 5.5, 12, 6], texture: '#1'},
},
},
{
- from: [7, 7, 0],
- to: [9, 11, 2],
+ from: [4, 17, 4],
+ to: [12, 20, 12],
faces: {
- north: {uv: [0.25, 0.25, 0.75, 1.25], rotation: 180, texture: '#0'},
- east: {uv: [0, 0.25, 0.25, 1.25], rotation: 180, texture: '#0'},
- south: {uv: [1, 0.25, 1.5, 1.25], rotation: 180, texture: '#0'},
- west: {uv: [0.75, 0.25, 1, 1.25], rotation: 180, texture: '#0'},
- up: {uv: [0.25, 0, 0.75, 0.25], rotation: 180, texture: '#0'},
- down: {uv: [0.75, 0, 1.25, 0.25], rotation: 180, texture: '#0'},
+ north: {uv: [0, 4, 4, 5.5], texture: '#1'},
+ east: {uv: [4, 4, 8, 5.5], texture: '#1'},
+ south: {uv: [8, 4, 12, 5.5], texture: '#1'},
+ west: {uv: [12, 4, 16, 5.5], texture: '#1'},
+ up: {uv: [4, 0, 8, 4], texture: '#1'},
+ down: {uv: [8, 0, 12, 4], texture: '#1'},
},
},
]).getMesh(atlas, Cull.none())
}
-}
-
-function decoratedPotRenderer(atlas: TextureAtlasProvider){
- return new BlockModel(undefined, {
- 0: 'entity/decorated_pot/decorated_pot_side',
- 1: 'entity/decorated_pot/decorated_pot_base',
- }, [
- {
- from: [1, 0, 1],
- to: [15, 16, 15],
- faces: {
- north: {uv: [1, 0, 15, 16], texture: '#0'},
- east: {uv: [1, 0, 15, 16], texture: '#0'},
- south: {uv: [1, 0, 15, 16], texture: '#0'},
- west: {uv: [1, 0, 15, 16], texture: '#0'},
- up: {uv: [0, 6.5, 7, 13.5], texture: '#1'},
- down: {uv: [7, 6.5, 14, 13.5], texture: '#1'},
- },
- },
- {
- from: [5, 16, 5],
- to: [11, 17, 11],
- faces: {
- north: {uv: [0, 5.5, 3, 6], texture: '#1'},
- east: {uv: [3, 5.5, 6, 6], texture: '#1'},
- south: {uv: [6, 5.5, 9, 6], texture: '#1'},
- west: {uv: [9, 5.5, 12, 6], texture: '#1'},
- },
- },
- {
- from: [4, 17, 4],
- to: [12, 20, 12],
- faces: {
- north: {uv: [0, 4, 4, 5.5], texture: '#1'},
- east: {uv: [4, 4, 8, 5.5], texture: '#1'},
- south: {uv: [8, 4, 12, 5.5], texture: '#1'},
- west: {uv: [12, 4, 16, 5.5], texture: '#1'},
- up: {uv: [4, 0, 8, 4], texture: '#1'},
- down: {uv: [8, 0, 12, 4], texture: '#1'},
- },
- },
- ]).getMesh(atlas, Cull.none())
-}
-
-function shieldRenderer(atlas: TextureAtlasProvider) {
- return new BlockModel(undefined, {
- 0: 'entity/shield_base_nopattern',
- }, [
- {
- from: [-6, -11, -2],
- to: [6, 11, -1],
- faces: {
- north: {uv: [3.5, 0.25, 6.5, 5.75], texture: '#0'},
- east: {uv: [3.25, 0.25, 3.5, 5.75], texture: '#0'},
- south: {uv: [0.25, 0.25, 3.25, 5.75], texture: '#0'},
- west: {uv: [0, 0.25, 0.25, 5.75], texture: '#0'},
- up: {uv: [0.25, 0, 3.25, 0.25], texture: '#0'},
- down: {uv: [3.25, 0, 6.25, 0.25], texture: '#0'},
- },
- },
- ]).getMesh(atlas, Cull.none())
-}
-function skullRenderer(texture: string, n: number) {
- return (atlas: TextureAtlasProvider) => {
+ export function shieldRenderer(atlas: TextureAtlasProvider) {
return new BlockModel(undefined, {
- 0: `entity/${texture}`,
+ 0: 'entity/shield_base_nopattern',
}, [
{
- from: [4, 0, 4],
- to: [12, 8, 12],
+ from: [-6, -11, -2],
+ to: [6, 11, -1],
faces: {
- north: {uv: [6, 2*n, 8, 4*n], texture: '#0'},
- east: {uv: [2, 2*n, 0, 4*n], texture: '#0'},
- south: {uv: [2, 2*n, 4, 4*n], texture: '#0'},
- west: {uv: [6, 2*n, 4, 4*n], texture: '#0'},
- up: {uv: [2, 0*n, 4, 2*n], texture: '#0'},
- down: {uv: [4, 0*n, 6, 2*n], texture: '#0'},
+ north: {uv: [3.5, 0.25, 6.5, 5.75], texture: '#0'},
+ east: {uv: [3.25, 0.25, 3.5, 5.75], texture: '#0'},
+ south: {uv: [0.25, 0.25, 3.25, 5.75], texture: '#0'},
+ west: {uv: [0, 0.25, 0.25, 5.75], texture: '#0'},
+ up: {uv: [0.25, 0, 3.25, 0.25], texture: '#0'},
+ down: {uv: [3.25, 0, 6.25, 0.25], texture: '#0'},
},
},
- ])
- .getMesh(atlas, Cull.none())
+ ]).getMesh(atlas, Cull.none())
}
-}
-function dragonHeadRenderer(atlas: TextureAtlasProvider) {
- const transformation = mat4.create()
- mat4.translate(transformation, transformation, [8, 8, 8])
- mat4.scale(transformation, transformation, [0.75, 0.75, 0.75])
- mat4.rotateY(transformation, transformation, Math.PI)
- mat4.translate(transformation, transformation, [-8, -11.2, -8])
- return new BlockModel(undefined, {
- 0: 'entity/enderdragon/dragon',
- }, [
- {
- from: [2, 4, -16],
- to: [14, 9, 0],
- faces: {
- north: {uv: [12, 3.75, 12.75, 4.0625], texture: '#0'},
- east: {uv: [11, 3.75, 12, 4.0625], texture: '#0'},
- south: {uv: [13.75, 3.75, 14.5, 4.0625], texture: '#0'},
- west: {uv: [12.75, 3.75, 13.75, 4.0625], texture: '#0'},
- up: {uv: [12.75, 3.75, 12, 2.75], texture: '#0'},
- down: {uv: [13.5, 2.75, 12.75, 3.75], texture: '#0'},
- },
- },
- {
- from: [0, 0, -2],
- to: [16, 16, 14],
- faces: {
- north: {uv: [8, 2.875, 9, 3.875], texture: '#0'},
- east: {uv: [7, 2.875, 8, 3.875], texture: '#0'},
- south: {uv: [10, 2.875, 11, 3.875], texture: '#0'},
- west: {uv: [9, 2.875, 10, 3.875], texture: '#0'},
- up: {uv: [9, 2.875, 8, 1.875], texture: '#0'},
- down: {uv: [10, 1.875, 9, 2.875], texture: '#0'},
- },
- },
- {
- from: [2, 0, -16],
- to: [14, 4, 0],
- rotation: {angle: -0.2 * 180 / Math.PI, axis: 'x', origin: [8, 4, -2]},
- faces: {
- north: {uv: [12, 5.0625, 12.75, 5.3125], texture: '#0'},
- east: {uv: [11, 5.0625, 12, 5.3125], texture: '#0'},
- south: {uv: [13.75, 5.0625, 14.5, 5.3125], texture: '#0'},
- west: {uv: [12.75, 5.0625, 13.75, 5.3125], texture: '#0'},
- up: {uv: [12.75, 5.0625, 12, 4.0625], texture: '#0'},
- down: {uv: [13.5, 4.0625, 12.75, 5.0625], texture: '#0'},
- },
- },
- {
- from: [3, 16, 4],
- to: [5, 20, 10],
- faces: {
- north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'},
- east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'},
- south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'},
- west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'},
- up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'},
- down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'},
- },
- },
- {
- from: [11, 16, 4],
- to: [13, 20, 10],
- faces: {
- north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'},
- east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'},
- south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'},
- west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'},
- up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'},
- down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'},
- },
- },
- {
- from: [3, 9, -14],
- to: [5, 11, -10],
- faces: {
- north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'},
- east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'},
- south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'},
- west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'},
- up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'},
- down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'},
- },
- },
- {
- from: [11, 9, -14],
- to: [13, 11, -10],
- faces: {
- north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'},
- east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'},
- south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'},
- west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'},
- up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'},
- down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'},
- },
- },
- ]).withUvEpsilon(1/256).getMesh(atlas, Cull.none()).transform(transformation)
-}
+ export function headRenderer(texture: Identifier, n: number) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/').toString(),
+ }, [
+ {
+ from: [4, 0, 4],
+ to: [12, 8, 12],
+ faces: {
+ north: {uv: [6, 2*n, 8, 4*n], texture: '#0'},
+ east: {uv: [2, 2*n, 0, 4*n], texture: '#0'},
+ south: {uv: [2, 2*n, 4, 4*n], texture: '#0'},
+ west: {uv: [6, 2*n, 4, 4*n], texture: '#0'},
+ up: {uv: [2, 0*n, 4, 2*n], texture: '#0'},
+ down: {uv: [4, 0*n, 6, 2*n], texture: '#0'},
+ },
+ },
+ ])
+ .getMesh(atlas, Cull.none())
+ }
+ }
-function piglinHeadRenderer(atlas: TextureAtlasProvider) {
- return new BlockModel(undefined, {
- 0: 'entity/piglin/piglin',
- }, [
- {
- from: [3, 0, 4],
- to: [13, 8, 12],
- faces: {
- north: {uv: [6.5, 2, 9, 4], texture: '#0'},
- east: {uv: [2, 2, 0, 4], texture: '#0'},
- south: {uv: [2, 2, 4.5, 4], texture: '#0'},
- west: {uv: [6.5, 2, 4.5, 4], texture: '#0'},
- up: {uv: [2, 0, 4.5, 2], texture: '#0'},
- down: {uv: [4.5, 0, 7, 2], texture: '#0'},
- },
- },
- {
- from: [6, 0, 12],
- to: [10, 4, 13],
- faces: {
- north: {uv: [9.25, 0.5, 10.25, 1.5], texture: '#0'},
- east: {uv: [7.75, 0.5, 8, 1.5], texture: '#0'},
- south: {uv: [8, 0.5, 9, 1.5], texture: '#0'},
- west: {uv: [9, 0.5, 9.25, 1.5], texture: '#0'},
- up: {uv: [8, 0.25, 9, 0.5], texture: '#0'},
- down: {uv: [9, 0.25, 10, 0.5], texture: '#0'},
- },
- },
- {
- from: [5, 0, 12],
- to: [6, 2, 13],
- faces: {
- north: {uv: [1.25, 0.25, 1.5, 0.75], texture: '#0'},
- east: {uv: [0.5, 0.25, 0.75, 0.75], texture: '#0'},
- south: {uv: [0.75, 0.25, 1, 0.75], texture: '#0'},
- west: {uv: [1, 0.25, 1.25, 0.75], texture: '#0'},
- up: {uv: [0.75, 0, 1, 0.25], texture: '#0'},
- down: {uv: [1, 0, 1.25, 0.25], texture: '#0'},
- },
- },
- {
- from: [10, 0, 12],
- to: [11, 2, 13],
- faces: {
- north: {uv: [1.25, 1.25, 1.5, 1.75], texture: '#0'},
- east: {uv: [0.5, 1.25, 0.75, 1.75], texture: '#0'},
- south: {uv: [0.75, 1.25, 1, 1.75], texture: '#0'},
- west: {uv: [1, 1.25, 1.25, 1.75], texture: '#0'},
- up: {uv: [0.75, 1, 1, 1.25], texture: '#0'},
- down: {uv: [1, 1, 1.25, 1.25], texture: '#0'},
- },
- },
- {
- from: [2.5, 1.5, 6],
- to: [3.5, 6.5, 10],
- rotation: {angle: -30, axis: 'z', origin: [3, 7, 8]},
- faces: {
- north: {uv: [12, 2.5, 12.25, 3.75], texture: '#0'},
- east: {uv: [9.75, 2.5, 10.75, 3.75], texture: '#0'},
- south: {uv: [10.75, 2.5, 11, 3.75], texture: '#0'},
- west: {uv: [11, 2.5, 12, 3.75], texture: '#0'},
- up: {uv: [10.75, 1.5, 11, 2.5], texture: '#0'},
- down: {uv: [11, 1.5, 11.25, 2.5], texture: '#0'},
- },
- },
- {
- from: [12.5, 1.5, 6],
- to: [13.5, 6.5, 10],
- rotation: {angle: 30, axis: 'z', origin: [13, 7, 8]},
- faces: {
- north: {uv: [15.25, 2.5, 15, 3.75], texture: '#0'},
- east: {uv: [15, 2.5, 14, 3.75], texture: '#0'},
- south: {uv: [14, 2.5, 13.75, 3.75], texture: '#0'},
- west: {uv: [13.75, 2.5, 12.75, 3.75], texture: '#0'},
- up: {uv: [14, 1.5, 13.75, 2.5], texture: '#0'},
- down: {uv: [14.25, 1.5, 14, 2.5], texture: '#0'},
- },
- },
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
-}
+ export function dragonHeadRenderer(texture: Identifier = Identifier.create('enderdragon/dragon')) {
+ return (atlas: TextureAtlasProvider) => {
+ const transformation = mat4.create()
+ mat4.translate(transformation, transformation, [8, 8, 8])
+ mat4.scale(transformation, transformation, [0.75, 0.75, 0.75])
+ mat4.rotateY(transformation, transformation, Math.PI)
+ mat4.translate(transformation, transformation, [-8, -11.2, -8])
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/').toString(),
+ }, [
+ {
+ from: [2, 4, -16],
+ to: [14, 9, 0],
+ faces: {
+ north: {uv: [12, 3.75, 12.75, 4.0625], texture: '#0'},
+ east: {uv: [11, 3.75, 12, 4.0625], texture: '#0'},
+ south: {uv: [13.75, 3.75, 14.5, 4.0625], texture: '#0'},
+ west: {uv: [12.75, 3.75, 13.75, 4.0625], texture: '#0'},
+ up: {uv: [12.75, 3.75, 12, 2.75], texture: '#0'},
+ down: {uv: [13.5, 2.75, 12.75, 3.75], texture: '#0'},
+ },
+ },
+ {
+ from: [0, 0, -2],
+ to: [16, 16, 14],
+ faces: {
+ north: {uv: [8, 2.875, 9, 3.875], texture: '#0'},
+ east: {uv: [7, 2.875, 8, 3.875], texture: '#0'},
+ south: {uv: [10, 2.875, 11, 3.875], texture: '#0'},
+ west: {uv: [9, 2.875, 10, 3.875], texture: '#0'},
+ up: {uv: [9, 2.875, 8, 1.875], texture: '#0'},
+ down: {uv: [10, 1.875, 9, 2.875], texture: '#0'},
+ },
+ },
+ {
+ from: [2, 0, -16],
+ to: [14, 4, 0],
+ rotation: {angle: -0.2 * 180 / Math.PI, axis: 'x', origin: [8, 4, -2]},
+ faces: {
+ north: {uv: [12, 5.0625, 12.75, 5.3125], texture: '#0'},
+ east: {uv: [11, 5.0625, 12, 5.3125], texture: '#0'},
+ south: {uv: [13.75, 5.0625, 14.5, 5.3125], texture: '#0'},
+ west: {uv: [12.75, 5.0625, 13.75, 5.3125], texture: '#0'},
+ up: {uv: [12.75, 5.0625, 12, 4.0625], texture: '#0'},
+ down: {uv: [13.5, 4.0625, 12.75, 5.0625], texture: '#0'},
+ },
+ },
+ {
+ from: [3, 16, 4],
+ to: [5, 20, 10],
+ faces: {
+ north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'},
+ east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'},
+ south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'},
+ west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'},
+ up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'},
+ down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'},
+ },
+ },
+ {
+ from: [11, 16, 4],
+ to: [13, 20, 10],
+ faces: {
+ north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'},
+ east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'},
+ south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'},
+ west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'},
+ up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'},
+ down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'},
+ },
+ },
+ {
+ from: [3, 9, -14],
+ to: [5, 11, -10],
+ faces: {
+ north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'},
+ east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'},
+ south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'},
+ west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'},
+ up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'},
+ down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'},
+ },
+ },
+ {
+ from: [11, 9, -14],
+ to: [13, 11, -10],
+ faces: {
+ north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'},
+ east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'},
+ south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'},
+ west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'},
+ up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'},
+ down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'},
+ },
+ },
+ ]).withUvEpsilon(1/256).getMesh(atlas, Cull.none()).transform(transformation)
+ }
+ }
-function signRenderer(woodType: string) {
- return (atlas: TextureAtlasProvider) => {
- return new BlockModel(undefined, {
- 0: `entity/signs/${woodType}`,
- }, [
- {
- from: [-4, 8, 7],
- to: [20, 20, 9],
- faces: {
- north: {uv: [0.5, 1, 6.5, 7], texture: '#0'},
- east: {uv: [0, 1, 0.5, 7], texture: '#0'},
- south: {uv: [7, 1, 13, 7], texture: '#0'},
- west: {uv: [6.5, 1, 7, 7], texture: '#0'},
- up: {uv: [6.5, 1, 0.5, 0], texture: '#0'},
- down: {uv: [12.5, 0, 6.5, 1], texture: '#0'},
+ export function piglinHeadRenderer(texture: Identifier = Identifier.create('piglin/piglin')) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/').toString(),
+ }, [
+ {
+ from: [3, 0, 4],
+ to: [13, 8, 12],
+ faces: {
+ north: {uv: [6.5, 2, 9, 4], texture: '#0'},
+ east: {uv: [2, 2, 0, 4], texture: '#0'},
+ south: {uv: [2, 2, 4.5, 4], texture: '#0'},
+ west: {uv: [6.5, 2, 4.5, 4], texture: '#0'},
+ up: {uv: [2, 0, 4.5, 2], texture: '#0'},
+ down: {uv: [4.5, 0, 7, 2], texture: '#0'},
+ },
},
- },
- {
- from: [7, -6, 7],
- to: [9, 8, 9],
- faces: {
- north: {uv: [0.5, 8, 1, 15], texture: '#0'},
- east: {uv: [0, 8, 0.5, 15], texture: '#0'},
- south: {uv: [1.5, 8, 2, 15], texture: '#0'},
- west: {uv: [1, 8, 1.5, 15], texture: '#0'},
- up: {uv: [1, 8, 0.5, 7], texture: '#0'},
- down: {uv: [1.5, 7, 1, 8], texture: '#0'},
+ {
+ from: [6, 0, 12],
+ to: [10, 4, 13],
+ faces: {
+ north: {uv: [9.25, 0.5, 10.25, 1.5], texture: '#0'},
+ east: {uv: [7.75, 0.5, 8, 1.5], texture: '#0'},
+ south: {uv: [8, 0.5, 9, 1.5], texture: '#0'},
+ west: {uv: [9, 0.5, 9.25, 1.5], texture: '#0'},
+ up: {uv: [8, 0.25, 9, 0.5], texture: '#0'},
+ down: {uv: [9, 0.25, 10, 0.5], texture: '#0'},
+ },
},
- },
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ {
+ from: [5, 0, 12],
+ to: [6, 2, 13],
+ faces: {
+ north: {uv: [1.25, 0.25, 1.5, 0.75], texture: '#0'},
+ east: {uv: [0.5, 0.25, 0.75, 0.75], texture: '#0'},
+ south: {uv: [0.75, 0.25, 1, 0.75], texture: '#0'},
+ west: {uv: [1, 0.25, 1.25, 0.75], texture: '#0'},
+ up: {uv: [0.75, 0, 1, 0.25], texture: '#0'},
+ down: {uv: [1, 0, 1.25, 0.25], texture: '#0'},
+ },
+ },
+ {
+ from: [10, 0, 12],
+ to: [11, 2, 13],
+ faces: {
+ north: {uv: [1.25, 1.25, 1.5, 1.75], texture: '#0'},
+ east: {uv: [0.5, 1.25, 0.75, 1.75], texture: '#0'},
+ south: {uv: [0.75, 1.25, 1, 1.75], texture: '#0'},
+ west: {uv: [1, 1.25, 1.25, 1.75], texture: '#0'},
+ up: {uv: [0.75, 1, 1, 1.25], texture: '#0'},
+ down: {uv: [1, 1, 1.25, 1.25], texture: '#0'},
+ },
+ },
+ {
+ from: [2.5, 1.5, 6],
+ to: [3.5, 6.5, 10],
+ rotation: {angle: -30, axis: 'z', origin: [3, 7, 8]},
+ faces: {
+ north: {uv: [12, 2.5, 12.25, 3.75], texture: '#0'},
+ east: {uv: [9.75, 2.5, 10.75, 3.75], texture: '#0'},
+ south: {uv: [10.75, 2.5, 11, 3.75], texture: '#0'},
+ west: {uv: [11, 2.5, 12, 3.75], texture: '#0'},
+ up: {uv: [10.75, 1.5, 11, 2.5], texture: '#0'},
+ down: {uv: [11, 1.5, 11.25, 2.5], texture: '#0'},
+ },
+ },
+ {
+ from: [12.5, 1.5, 6],
+ to: [13.5, 6.5, 10],
+ rotation: {angle: 30, axis: 'z', origin: [13, 7, 8]},
+ faces: {
+ north: {uv: [15.25, 2.5, 15, 3.75], texture: '#0'},
+ east: {uv: [15, 2.5, 14, 3.75], texture: '#0'},
+ south: {uv: [14, 2.5, 13.75, 3.75], texture: '#0'},
+ west: {uv: [13.75, 2.5, 12.75, 3.75], texture: '#0'},
+ up: {uv: [14, 1.5, 13.75, 2.5], texture: '#0'},
+ down: {uv: [14.25, 1.5, 14, 2.5], texture: '#0'},
+ },
+ },
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
}
-}
-function wallSignRenderer(woodType: string) {
- return (atlas: TextureAtlasProvider) => {
- return new BlockModel(undefined, {
- 0: `entity/signs/${woodType}`,
- }, [
- {
- from: [-4, 4, 17],
- to: [20, 16, 19],
- faces: {
- north: {uv: [0.5, 1, 6.5, 7], texture: '#0'},
- east: {uv: [0, 1, 0.5, 7], texture: '#0'},
- south: {uv: [7, 1, 13, 7], texture: '#0'},
- west: {uv: [6.5, 1, 7, 7], texture: '#0'},
- up: {uv: [6.5, 1, 0.5, 0], texture: '#0'},
- down: {uv: [12.5, 0, 6.5, 1], texture: '#0'},
+ export function signRenderer(texture: Identifier) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/signs/').toString(),
+ }, [
+ {
+ from: [-4, 8, 7],
+ to: [20, 20, 9],
+ faces: {
+ north: {uv: [0.5, 1, 6.5, 7], texture: '#0'},
+ east: {uv: [0, 1, 0.5, 7], texture: '#0'},
+ south: {uv: [7, 1, 13, 7], texture: '#0'},
+ west: {uv: [6.5, 1, 7, 7], texture: '#0'},
+ up: {uv: [6.5, 1, 0.5, 0], texture: '#0'},
+ down: {uv: [12.5, 0, 6.5, 1], texture: '#0'},
+ },
},
- },
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ {
+ from: [7, -6, 7],
+ to: [9, 8, 9],
+ faces: {
+ north: {uv: [0.5, 8, 1, 15], texture: '#0'},
+ east: {uv: [0, 8, 0.5, 15], texture: '#0'},
+ south: {uv: [1.5, 8, 2, 15], texture: '#0'},
+ west: {uv: [1, 8, 1.5, 15], texture: '#0'},
+ up: {uv: [1, 8, 0.5, 7], texture: '#0'},
+ down: {uv: [1.5, 7, 1, 8], texture: '#0'},
+ },
+ },
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
}
-}
-function hangingSignRenderer(woodType: string) {
- return (attached: boolean, atlas: TextureAtlasProvider) => {
- if (attached) {
+ export function wallSignRenderer(texture: Identifier) {
+ return (atlas: TextureAtlasProvider) => {
return new BlockModel(undefined, {
- 0: `entity/signs/hanging/${woodType}`,
+ 0: texture.withPrefix('entity/signs/').toString(),
+ }, [
+ {
+ from: [-4, 4, 17],
+ to: [20, 16, 19],
+ faces: {
+ north: {uv: [0.5, 1, 6.5, 7], texture: '#0'},
+ east: {uv: [0, 1, 0.5, 7], texture: '#0'},
+ south: {uv: [7, 1, 13, 7], texture: '#0'},
+ west: {uv: [6.5, 1, 7, 7], texture: '#0'},
+ up: {uv: [6.5, 1, 0.5, 0], texture: '#0'},
+ down: {uv: [12.5, 0, 6.5, 1], texture: '#0'},
+ },
+ },
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
+ }
+
+ export function hangingSignRenderer(texture: Identifier) {
+ return (attached: boolean, atlas: TextureAtlasProvider) => {
+ if (attached) {
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/signs/hanging/').toString(),
+ }, [
+ {
+ from: [1, 0, 7],
+ to: [15, 10, 9],
+ faces: {
+ north: {uv: [0.5, 7, 4, 12], texture: '#0'},
+ east: {uv: [0, 7, 0.5, 12], texture: '#0'},
+ south: {uv: [4.5, 7, 8, 12], texture: '#0'},
+ west: {uv: [4, 7, 4.5, 12], texture: '#0'},
+ up: {uv: [4, 7, 0.5, 6], texture: '#0'},
+ down: {uv: [7.5, 6, 4, 7], texture: '#0'},
+ },
+ },
+ {
+ from: [2, 10, 8],
+ to: [14, 16, 8],
+ faces: {
+ north: {uv: [3.5, 3, 6.5, 6], texture: '#0'},
+ south: {uv: [3.5, 3, 6.5, 6], texture: '#0'},
+ },
+ },
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/signs/hanging/').toString(),
}, [
{
from: [1, 0, 7],
@@ -405,476 +435,451 @@ function hangingSignRenderer(woodType: string) {
},
},
{
- from: [2, 10, 8],
- to: [14, 16, 8],
+ from: [1.5, 10, 8],
+ to: [4.5, 16, 8],
+ rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
faces: {
- north: {uv: [3.5, 3, 6.5, 6], texture: '#0'},
- south: {uv: [3.5, 3, 6.5, 6], texture: '#0'},
+ north: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ south: {uv: [0, 3, 0.75, 6], texture: '#0'},
},
},
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
- }
- return new BlockModel(undefined, {
- 0: `entity/signs/hanging/${woodType}`,
- }, [
- {
- from: [1, 0, 7],
- to: [15, 10, 9],
- faces: {
- north: {uv: [0.5, 7, 4, 12], texture: '#0'},
- east: {uv: [0, 7, 0.5, 12], texture: '#0'},
- south: {uv: [4.5, 7, 8, 12], texture: '#0'},
- west: {uv: [4, 7, 4.5, 12], texture: '#0'},
- up: {uv: [4, 7, 0.5, 6], texture: '#0'},
- down: {uv: [7.5, 6, 4, 7], texture: '#0'},
- },
- },
- {
- from: [1.5, 10, 8],
- to: [4.5, 16, 8],
- rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
- faces: {
- north: {uv: [0, 3, 0.75, 6], texture: '#0'},
- south: {uv: [0, 3, 0.75, 6], texture: '#0'},
- },
- },
- {
- from: [3, 10, 6.5],
- to: [3, 16, 9.5],
- rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
- faces: {
- east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
- west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ {
+ from: [3, 10, 6.5],
+ to: [3, 16, 9.5],
+ rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
+ faces: {
+ east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ },
},
- },
- {
- from: [11.5, 10, 8],
- to: [14.5, 16, 8],
- rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
- faces: {
- north: {uv: [0, 3, 0.75, 6], texture: '#0'},
- south: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ {
+ from: [11.5, 10, 8],
+ to: [14.5, 16, 8],
+ rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
+ faces: {
+ north: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ south: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ },
},
- },
- {
- from: [13, 10, 6.5],
- to: [13, 16, 9.5],
- rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
- faces: {
- east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
- west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ {
+ from: [13, 10, 6.5],
+ to: [13, 16, 9.5],
+ rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
+ faces: {
+ east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ },
},
- },
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
}
-}
-function wallHangingSignRenderer(woodType: string) {
- return (atlas: TextureAtlasProvider) => {
- return new BlockModel(undefined, {
- 0: `entity/signs/hanging/${woodType}`,
- }, [
- {
- from: [1, 0, 7],
- to: [15, 10, 9],
- faces: {
- north: {uv: [0.5, 7, 4, 12], texture: '#0'},
- east: {uv: [0, 7, 0.5, 12], texture: '#0'},
- south: {uv: [4.5, 7, 8, 12], texture: '#0'},
- west: {uv: [4, 7, 4.5, 12], texture: '#0'},
- up: {uv: [4, 7, 0.5, 6], texture: '#0'},
- down: {uv: [7.5, 6, 4, 7], texture: '#0'},
+ export function wallHangingSignRenderer(woodType: string) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: `entity/signs/hanging/${woodType}`,
+ }, [
+ {
+ from: [1, 0, 7],
+ to: [15, 10, 9],
+ faces: {
+ north: {uv: [0.5, 7, 4, 12], texture: '#0'},
+ east: {uv: [0, 7, 0.5, 12], texture: '#0'},
+ south: {uv: [4.5, 7, 8, 12], texture: '#0'},
+ west: {uv: [4, 7, 4.5, 12], texture: '#0'},
+ up: {uv: [4, 7, 0.5, 6], texture: '#0'},
+ down: {uv: [7.5, 6, 4, 7], texture: '#0'},
+ },
},
- },
- {
- from: [0, 14, 6],
- to: [16, 16, 10],
- faces: {
- north: {uv: [1, 2, 5, 3], texture: '#0'},
- east: {uv: [0, 2, 1, 3], texture: '#0'},
- south: {uv: [6, 2, 10, 3], texture: '#0'},
- west: {uv: [5, 2, 6, 3], texture: '#0'},
- up: {uv: [5, 2, 1, 0], texture: '#0'},
- down: {uv: [9, 0, 5, 2], texture: '#0'},
+ {
+ from: [0, 14, 6],
+ to: [16, 16, 10],
+ faces: {
+ north: {uv: [1, 2, 5, 3], texture: '#0'},
+ east: {uv: [0, 2, 1, 3], texture: '#0'},
+ south: {uv: [6, 2, 10, 3], texture: '#0'},
+ west: {uv: [5, 2, 6, 3], texture: '#0'},
+ up: {uv: [5, 2, 1, 0], texture: '#0'},
+ down: {uv: [9, 0, 5, 2], texture: '#0'},
+ },
},
- },
- {
- from: [1.5, 10, 8],
- to: [4.5, 16, 8],
- rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
- faces: {
- north: {uv: [0, 3, 0.75, 6], texture: '#0'},
- south: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ {
+ from: [1.5, 10, 8],
+ to: [4.5, 16, 8],
+ rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
+ faces: {
+ north: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ south: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ },
},
- },
- {
- from: [3, 10, 6.5],
- to: [3, 16, 9.5],
- rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
- faces: {
- east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
- west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ {
+ from: [3, 10, 6.5],
+ to: [3, 16, 9.5],
+ rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]},
+ faces: {
+ east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ },
},
- },
- {
- from: [11.5, 10, 8],
- to: [14.5, 16, 8],
- rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
- faces: {
- north: {uv: [0, 3, 0.75, 6], texture: '#0'},
- south: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ {
+ from: [11.5, 10, 8],
+ to: [14.5, 16, 8],
+ rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
+ faces: {
+ north: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ south: {uv: [0, 3, 0.75, 6], texture: '#0'},
+ },
},
- },
- {
- from: [13, 10, 6.5],
- to: [13, 16, 9.5],
- rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
- faces: {
- east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
- west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ {
+ from: [13, 10, 6.5],
+ to: [13, 16, 9.5],
+ rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]},
+ faces: {
+ east: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ west: {uv: [1.5, 3, 2.25, 6], texture: '#0'},
+ },
},
- },
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
}
-}
-function conduitRenderer(atlas: TextureAtlasProvider) {
- return new BlockModel(undefined, {
- 0: 'entity/conduit/base',
- }, [
- {
- from: [5, 5, 5],
- to: [11, 11, 11],
- faces: {
- north: {uv: [3, 6, 6, 12], texture: '#0'},
- east: {uv: [0, 6, 3, 12], texture: '#0'},
- south: {uv: [9, 6, 12, 12], texture: '#0'},
- west: {uv: [6, 6, 9, 12], texture: '#0'},
- up: {uv: [6, 6, 3, 0], texture: '#0'},
- down: {uv: [9, 0, 6, 6], texture: '#0'},
- },
- },
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
-}
-
-function shulkerBoxRenderer(color: string) {
- return (atlas: TextureAtlasProvider) => {
+ export function conduitRenderer(atlas: TextureAtlasProvider) {
return new BlockModel(undefined, {
- 0: `entity/shulker/shulker_${color}`,
+ 0: 'entity/conduit/base',
}, [
{
- from: [0, 0, 0],
- to: [16, 8, 16],
- faces: {
- north: {uv: [4, 11, 8, 13], texture: '#0'},
- east: {uv: [0, 11, 4, 13], texture: '#0'},
- south: {uv: [12, 11, 16, 13], texture: '#0'},
- west: {uv: [8, 11, 12, 13], texture: '#0'},
- up: {uv: [8, 11, 4, 7], texture: '#0'},
- down: {uv: [12, 7, 8, 11], texture: '#0'},
- },
- },
- {
- from: [0, 4, 0],
- to: [16, 16, 16],
+ from: [5, 5, 5],
+ to: [11, 11, 11],
faces: {
- north: {uv: [4, 4, 8, 7], texture: '#0'},
- east: {uv: [0, 4, 4, 7], texture: '#0'},
- south: {uv: [12, 4, 16, 7], texture: '#0'},
- west: {uv: [8, 4, 12, 7], texture: '#0'},
- up: {uv: [8, 4, 4, 0], texture: '#0'},
- down: {uv: [12, 0, 8, 4], texture: '#0'},
+ north: {uv: [3, 6, 6, 12], texture: '#0'},
+ east: {uv: [0, 6, 3, 12], texture: '#0'},
+ south: {uv: [9, 6, 12, 12], texture: '#0'},
+ west: {uv: [6, 6, 9, 12], texture: '#0'},
+ up: {uv: [6, 6, 3, 0], texture: '#0'},
+ down: {uv: [9, 0, 6, 6], texture: '#0'},
},
},
]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
}
-}
-function bannerRenderer(color: string) {
- return (atlas: TextureAtlasProvider) => {
- return new BlockModel(undefined, {
- 0: 'entity/banner_base',
- }, [
- {
- from: [-2, -8, 9],
- to: [18, 32, 10],
- faces: {
- north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'},
- east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'},
- south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'},
- west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'},
- up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'},
- down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'},
+ export function shulkerBoxRenderer(texture: Identifier) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/shulker/').toString(),
+ }, [
+ {
+ from: [0, 0, 0],
+ to: [16, 8, 16],
+ faces: {
+ north: {uv: [4, 11, 8, 13], texture: '#0'},
+ east: {uv: [0, 11, 4, 13], texture: '#0'},
+ south: {uv: [12, 11, 16, 13], texture: '#0'},
+ west: {uv: [8, 11, 12, 13], texture: '#0'},
+ up: {uv: [8, 11, 4, 7], texture: '#0'},
+ down: {uv: [12, 7, 8, 11], texture: '#0'},
+ },
},
- },
- {
- from: [7, -12, 7],
- to: [9, 30, 9],
- faces: {
- north: {uv: [11.5, 0.5, 12, 11], texture: '#0'},
- east: {uv: [11, 0.5, 11.5, 11], texture: '#0'},
- south: {uv: [12.5, 0.5, 13, 11], texture: '#0'},
- west: {uv: [12, 0.5, 12.5, 11], texture: '#0'},
- up: {uv: [12, 0.5, 11.5, 0], texture: '#0'},
- down: {uv: [12.5, 0, 12, 0.5], texture: '#0'},
+ {
+ from: [0, 4, 0],
+ to: [16, 16, 16],
+ faces: {
+ north: {uv: [4, 4, 8, 7], texture: '#0'},
+ east: {uv: [0, 4, 4, 7], texture: '#0'},
+ south: {uv: [12, 4, 16, 7], texture: '#0'},
+ west: {uv: [8, 4, 12, 7], texture: '#0'},
+ up: {uv: [8, 4, 4, 0], texture: '#0'},
+ down: {uv: [12, 0, 8, 4], texture: '#0'},
+ },
},
- },
- {
- from: [-2, 30, 7],
- to: [18, 32, 9],
- faces: {
- north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'},
- east: {uv: [0, 11, 0.5, 11.5], texture: '#0'},
- south: {uv: [6, 11, 11, 11.5], texture: '#0'},
- west: {uv: [5.5, 11, 6, 11.5], texture: '#0'},
- up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'},
- down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'},
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
+ }
+
+ export function bannerRenderer(color: string) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: 'entity/banner_base',
+ }, [
+ {
+ from: [-2, -8, 9],
+ to: [18, 32, 10],
+ faces: {
+ north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'},
+ east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'},
+ south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'},
+ west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'},
+ up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'},
+ down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'},
+ },
},
- },
- ]).getMesh(atlas, Cull.none())
+ {
+ from: [7, -12, 7],
+ to: [9, 30, 9],
+ faces: {
+ north: {uv: [11.5, 0.5, 12, 11], texture: '#0'},
+ east: {uv: [11, 0.5, 11.5, 11], texture: '#0'},
+ south: {uv: [12.5, 0.5, 13, 11], texture: '#0'},
+ west: {uv: [12, 0.5, 12.5, 11], texture: '#0'},
+ up: {uv: [12, 0.5, 11.5, 0], texture: '#0'},
+ down: {uv: [12.5, 0, 12, 0.5], texture: '#0'},
+ },
+ },
+ {
+ from: [-2, 30, 7],
+ to: [18, 32, 9],
+ faces: {
+ north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'},
+ east: {uv: [0, 11, 0.5, 11.5], texture: '#0'},
+ south: {uv: [6, 11, 11, 11.5], texture: '#0'},
+ west: {uv: [5.5, 11, 6, 11.5], texture: '#0'},
+ up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'},
+ down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'},
+ },
+ },
+ ]).getMesh(atlas, Cull.none())
+ }
+ }
+
+ export function wallBannerRenderer(color: string) {
+ return (atlas: TextureAtlasProvider) => {
+ return new BlockModel(undefined, {
+ 0: 'entity/banner_base',
+ }, [
+ {
+ from: [-2, -8, -1.5],
+ to: [18, 32, -0.5],
+ faces: {
+ north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'},
+ east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'},
+ south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'},
+ west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'},
+ up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'},
+ down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'},
+ },
+ },
+ {
+ from: [-2, 30, -3.5],
+ to: [18, 32, -1.5],
+ faces: {
+ north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'},
+ east: {uv: [0, 11, 0.5, 11.5], texture: '#0'},
+ south: {uv: [6, 11, 11, 11.5], texture: '#0'},
+ west: {uv: [5.5, 11, 6, 11.5], texture: '#0'},
+ up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'},
+ down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'},
+ },
+ },
+ ]).getMesh(atlas, Cull.none())
+ }
}
-}
-function wallBannerRenderer(color: string) {
- return (atlas: TextureAtlasProvider) => {
+ export function bellRenderer(atlas: TextureAtlasProvider) {
return new BlockModel(undefined, {
- 0: 'entity/banner_base',
+ 0: 'entity/bell/bell_body',
}, [
{
- from: [-2, -8, -1.5],
- to: [18, 32, -0.5],
+ from: [5, 3, 5],
+ to: [11, 10, 11],
faces: {
- north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'},
- east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'},
- south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'},
- west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'},
- up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'},
- down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'},
+ north: {uv: [3, 3, 6, 6.5], texture: '#0'},
+ east: {uv: [0, 3, 3, 6.5], texture: '#0'},
+ south: {uv: [9, 3, 12, 6.5], texture: '#0'},
+ west: {uv: [6, 3, 9, 6.5], texture: '#0'},
+ up: {uv: [6, 3, 3, 0], texture: '#0'},
+ down: {uv: [9, 0, 6, 3], texture: '#0'},
},
},
{
- from: [-2, 30, -3.5],
- to: [18, 32, -1.5],
+ from: [4, 10, 4],
+ to: [12, 12, 12],
faces: {
- north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'},
- east: {uv: [0, 11, 0.5, 11.5], texture: '#0'},
- south: {uv: [6, 11, 11, 11.5], texture: '#0'},
- west: {uv: [5.5, 11, 6, 11.5], texture: '#0'},
- up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'},
- down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'},
+ north: {uv: [4, 10.5, 8, 11.5], texture: '#0'},
+ east: {uv: [0, 10.5, 4, 11.5], texture: '#0'},
+ south: {uv: [12, 10.5, 16, 11.5], texture: '#0'},
+ west: {uv: [8, 10.5, 12, 11.5], texture: '#0'},
+ up: {uv: [8, 10.5, 4, 6.5], texture: '#0'},
+ down: {uv: [12, 6.5, 8, 10.5], texture: '#0'},
},
},
- ]).getMesh(atlas, Cull.none())
+ ]).withUvEpsilon(1 / 64).getMesh(atlas, Cull.none())
}
-}
-
-function bellRenderer(atlas: TextureAtlasProvider) {
- return new BlockModel(undefined, {
- 0: 'entity/bell/bell_body',
- }, [
- {
- from: [5, 3, 5],
- to: [11, 10, 11],
- faces: {
- north: {uv: [3, 3, 6, 6.5], texture: '#0'},
- east: {uv: [0, 3, 3, 6.5], texture: '#0'},
- south: {uv: [9, 3, 12, 6.5], texture: '#0'},
- west: {uv: [6, 3, 9, 6.5], texture: '#0'},
- up: {uv: [6, 3, 3, 0], texture: '#0'},
- down: {uv: [9, 0, 6, 3], texture: '#0'},
- },
- },
- {
- from: [4, 10, 4],
- to: [12, 12, 12],
- faces: {
- north: {uv: [4, 10.5, 8, 11.5], texture: '#0'},
- east: {uv: [0, 10.5, 4, 11.5], texture: '#0'},
- south: {uv: [12, 10.5, 16, 11.5], texture: '#0'},
- west: {uv: [8, 10.5, 12, 11.5], texture: '#0'},
- up: {uv: [8, 10.5, 4, 6.5], texture: '#0'},
- down: {uv: [12, 6.5, 8, 10.5], texture: '#0'},
- },
- },
- ]).withUvEpsilon(1/64).getMesh(atlas, Cull.none())
-}
-function bedRenderer(color: string) {
- return (part: string, atlas: TextureAtlasProvider) => {
- if (part === 'foot') {
+ export function bedRenderer(texture: Identifier) {
+ return (part: string, atlas: TextureAtlasProvider) => {
+ if (part === 'foot') {
+ return new BlockModel(undefined, {
+ 0: texture.withPrefix('entity/bed/').toString(),
+ }, [
+ {
+ from: [0, 3, 0],
+ to: [16, 9, 16],
+ faces: {
+ north: {uv: [5.5, 5.5, 9.5, 7], rotation: 180, texture: '#0'},
+ east: {uv: [0, 7, 1.5, 11], rotation: 270, texture: '#0'},
+ west: {uv: [5.5, 7, 7, 11], rotation: 90, texture: '#0'},
+ up: {uv: [5.5, 11, 1.5, 7], texture: '#0'},
+ down: {uv: [11, 7, 7, 11], texture: '#0'},
+ },
+ },
+ {
+ from: [0, 0, 0],
+ to: [3, 3, 3],
+ faces: {
+ north: {uv: [12.5, 5.25, 13.25, 6], texture: '#0'},
+ east: {uv: [14.75, 5.25, 15.5, 6], texture: '#0'},
+ south: {uv: [14, 5.25, 14.75, 6], texture: '#0'},
+ west: {uv: [13.25, 5.25, 14, 6], texture: '#0'},
+ up: {uv: [13.25, 4.5, 14, 5.25], texture: '#0'},
+ down: {uv: [14, 4.5, 14.75, 5.25], texture: '#0'},
+ },
+ },
+ {
+ from: [13, 0, 0],
+ to: [16, 3, 3],
+ faces: {
+ north: {uv: [13.25, 3.75, 14, 4.5], texture: '#0'},
+ east: {uv: [12.5, 3.75, 13.25, 4.5], texture: '#0'},
+ south: {uv: [14.75, 3.75, 15.5, 4.5], texture: '#0'},
+ west: {uv: [14, 3.75, 14.75, 4.5], texture: '#0'},
+ up: {uv: [13.25, 3, 14, 3.75], texture: '#0'},
+ down: {uv: [14, 3, 14.75, 3.75], texture: '#0'},
+ },
+ },
+ ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
+ }
return new BlockModel(undefined, {
- 0: `entity/bed/${color}`,
+ 0: texture.withPrefix('entity/bed/').toString(),
}, [
{
from: [0, 3, 0],
to: [16, 9, 16],
faces: {
- north: {uv: [5.5, 5.5, 9.5, 7], rotation: 180, texture: '#0'},
- east: {uv: [0, 7, 1.5, 11], rotation: 270, texture: '#0'},
- west: {uv: [5.5, 7, 7, 11], rotation: 90, texture: '#0'},
- up: {uv: [5.5, 11, 1.5, 7], texture: '#0'},
- down: {uv: [11, 7, 7, 11], texture: '#0'},
+ east: {uv: [0, 1.5, 1.5, 5.5], rotation: 270, texture: '#0'},
+ south: {uv: [1.5, 0, 5.5, 1.5], rotation: 180, texture: '#0'},
+ west: {uv: [5.5, 1.5, 7, 5.5], rotation: 90, texture: '#0'},
+ up: {uv: [5.5, 5.5, 1.5, 1.5], texture: '#0'},
+ down: {uv: [11, 1.5, 7, 5.5], texture: '#0'},
},
},
{
- from: [0, 0, 0],
- to: [3, 3, 3],
+ from: [0, 0, 13],
+ to: [3, 3, 16],
faces: {
- north: {uv: [12.5, 5.25, 13.25, 6], texture: '#0'},
- east: {uv: [14.75, 5.25, 15.5, 6], texture: '#0'},
- south: {uv: [14, 5.25, 14.75, 6], texture: '#0'},
- west: {uv: [13.25, 5.25, 14, 6], texture: '#0'},
- up: {uv: [13.25, 4.5, 14, 5.25], texture: '#0'},
- down: {uv: [14, 4.5, 14.75, 5.25], texture: '#0'},
+ north: {uv: [14.75, 0.75, 15.5, 1.5], texture: '#0'},
+ east: {uv: [14, 0.75, 14.75, 1.5], texture: '#0'},
+ south: {uv: [13.25, 0.75, 14, 1.5], texture: '#0'},
+ west: {uv: [12.5, 0.75, 13.25, 1.5], texture: '#0'},
+ up: {uv: [13.25, 0, 14, 0.75], texture: '#0'},
+ down: {uv: [14, 0, 14.75, 0.75], texture: '#0'},
},
},
{
- from: [13, 0, 0],
- to: [16, 3, 3],
+ from: [13, 0, 13],
+ to: [16, 3, 16],
faces: {
- north: {uv: [13.25, 3.75, 14, 4.5], texture: '#0'},
- east: {uv: [12.5, 3.75, 13.25, 4.5], texture: '#0'},
- south: {uv: [14.75, 3.75, 15.5, 4.5], texture: '#0'},
- west: {uv: [14, 3.75, 14.75, 4.5], texture: '#0'},
- up: {uv: [13.25, 3, 14, 3.75], texture: '#0'},
- down: {uv: [14, 3, 14.75, 3.75], texture: '#0'},
+ north: {uv: [14, 2.25, 14.75, 3], texture: '#0'},
+ east: {uv: [13.25, 2.25, 14, 3], texture: '#0'},
+ south: {uv: [12.5, 2.25, 13.25, 3], texture: '#0'},
+ west: {uv: [14.75, 2.25, 15.5, 3], texture: '#0'},
+ up: {uv: [13.25, 1.5, 14, 2.25], texture: '#0'},
+ down: {uv: [14, 1.5, 14.75, 2.25], texture: '#0'},
},
},
]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
}
- return new BlockModel(undefined, {
- 0: `entity/bed/${color}`,
- }, [
- {
- from: [0, 3, 0],
- to: [16, 9, 16],
- faces: {
- east: {uv: [0, 1.5, 1.5, 5.5], rotation: 270, texture: '#0'},
- south: {uv: [1.5, 0, 5.5, 1.5], rotation: 180, texture: '#0'},
- west: {uv: [5.5, 1.5, 7, 5.5], rotation: 90, texture: '#0'},
- up: {uv: [5.5, 5.5, 1.5, 1.5], texture: '#0'},
- down: {uv: [11, 1.5, 7, 5.5], texture: '#0'},
- },
- },
- {
- from: [0, 0, 13],
- to: [3, 3, 16],
- faces: {
- north: {uv: [14.75, 0.75, 15.5, 1.5], texture: '#0'},
- east: {uv: [14, 0.75, 14.75, 1.5], texture: '#0'},
- south: {uv: [13.25, 0.75, 14, 1.5], texture: '#0'},
- west: {uv: [12.5, 0.75, 13.25, 1.5], texture: '#0'},
- up: {uv: [13.25, 0, 14, 0.75], texture: '#0'},
- down: {uv: [14, 0, 14.75, 0.75], texture: '#0'},
- },
- },
- {
- from: [13, 0, 13],
- to: [16, 3, 16],
- faces: {
- north: {uv: [14, 2.25, 14.75, 3], texture: '#0'},
- east: {uv: [13.25, 2.25, 14, 3], texture: '#0'},
- south: {uv: [12.5, 2.25, 13.25, 3], texture: '#0'},
- west: {uv: [14.75, 2.25, 15.5, 3], texture: '#0'},
- up: {uv: [13.25, 1.5, 14, 2.25], texture: '#0'},
- down: {uv: [14, 1.5, 14.75, 2.25], texture: '#0'},
- },
- },
- ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none())
}
-}
-function getStr(block: BlockState, key: string, fallback = '') {
- return block.getProperty(key) ?? fallback
-}
-
-function getInt(block: BlockState, key: string, fallback = '0') {
- return parseInt(block.getProperty(key) ?? fallback)
-}
-
-const ChestRenderers = new Map(Object.entries({
- 'minecraft:chest': chestRenderer('normal'),
- 'minecraft:ender_chest': chestRenderer('ender'),
- 'minecraft:trapped_chest': chestRenderer('trapped'),
-}))
-
-const SkullRenderers = new Map(Object.entries({
- 'minecraft:skeleton_skull': skullRenderer('skeleton/skeleton', 2),
- 'minecraft:wither_skeleton_skull': skullRenderer('skeleton/wither_skeleton', 2),
- 'minecraft:zombie_head': skullRenderer('zombie/zombie', 1),
- 'minecraft:creeper_head': skullRenderer('creeper/creeper', 2),
- 'minecraft:dragon_head': dragonHeadRenderer,
- 'minecraft:piglin_head': piglinHeadRenderer,
- 'minecraft:player_head': skullRenderer('player/wide/steve', 1), // TODO: fix texture
-}))
-
-const WoodTypes = [
- 'oak',
- 'spruce',
- 'birch',
- 'jungle',
- 'acacia',
- 'dark_oak',
- 'mangrove',
- 'cherry',
- 'bamboo',
- 'crimson',
- 'warped',
-]
-
-const SignRenderers = new Map(WoodTypes.map(type =>
- [`minecraft:${type}_sign`, signRenderer(type)]
-))
-
-const WallSignRenderers = new Map(WoodTypes.map(type =>
- [`minecraft:${type}_wall_sign`, wallSignRenderer(type)]
-))
-
-const HangingSignRenderers = new Map(WoodTypes.map(type =>
- [`minecraft:${type}_hanging_sign`, hangingSignRenderer(type)]
-))
-
-const WallHangingSignRenderers = new Map(WoodTypes.map(type =>
- [`minecraft:${type}_wall_hanging_sign`, wallHangingSignRenderer(type)]
-))
-
-const DyeColors = [
- 'white',
- 'orange',
- 'magenta',
- 'light_blue',
- 'yellow',
- 'lime',
- 'pink',
- 'gray',
- 'light_gray',
- 'cyan',
- 'purple',
- 'blue',
- 'brown',
- 'green',
- 'red',
- 'black',
-]
-
-const ShulkerBoxRenderers = new Map(DyeColors.map(color =>
- [`minecraft:${color}_shulker_box`, shulkerBoxRenderer(color)]
-))
+ function getStr(block: BlockState, key: string, fallback = '') {
+ return block.getProperty(key) ?? fallback
+ }
-const BedRenderers = new Map(DyeColors.map(color =>
- [`minecraft:${color}_bed`, bedRenderer(color)]
-))
+ function getInt(block: BlockState, key: string, fallback = '0') {
+ return parseInt(block.getProperty(key) ?? fallback)
+ }
-const BannerRenderers = new Map(DyeColors.map(color =>
- [`minecraft:${color}_banner`, bannerRenderer(color)]
-))
+ const ChestRenderers = new Map(Object.entries({
+ 'minecraft:chest': SpecialRenderers.chestRenderer(Identifier.create('normal')),
+ 'minecraft:ender_chest': SpecialRenderers.chestRenderer(Identifier.create('ender')),
+ 'minecraft:trapped_chest': SpecialRenderers.chestRenderer(Identifier.create('trapped')),
+ }))
+
+ const SkullRenderers = new Map(Object.entries({
+ 'minecraft:skeleton_skull': SpecialRenderers.headRenderer(Identifier.create('skeleton/skeleton'), 2),
+ 'minecraft:wither_skeleton_skull': SpecialRenderers.headRenderer(Identifier.create('skeleton/wither_skeleton'), 2),
+ 'minecraft:zombie_head': SpecialRenderers.headRenderer(Identifier.create('zombie/zombie'), 1),
+ 'minecraft:creeper_head': SpecialRenderers.headRenderer(Identifier.create('creeper/creeper'), 2),
+ 'minecraft:dragon_head': SpecialRenderers.dragonHeadRenderer(),
+ 'minecraft:piglin_head': SpecialRenderers.piglinHeadRenderer(),
+ 'minecraft:player_head': SpecialRenderers.headRenderer(Identifier.create('player/wide/steve'), 1), // TODO: fix texture
+ }))
+
+ const WoodTypes = [
+ 'oak',
+ 'spruce',
+ 'birch',
+ 'jungle',
+ 'acacia',
+ 'dark_oak',
+ 'mangrove',
+ 'cherry',
+ 'bamboo',
+ 'crimson',
+ 'warped',
+ ]
+
+ const SignRenderers = new Map(WoodTypes.map(type =>
+ [`minecraft:${type}_sign`, SpecialRenderers.signRenderer(Identifier.create(type))]
+ ))
+
+ const WallSignRenderers = new Map(WoodTypes.map(type =>
+ [`minecraft:${type}_wall_sign`, SpecialRenderers.wallSignRenderer(Identifier.create(type))]
+ ))
+
+ const HangingSignRenderers = new Map(WoodTypes.map(type =>
+ [`minecraft:${type}_hanging_sign`, SpecialRenderers.hangingSignRenderer(Identifier.create(type))]
+ ))
+
+ const WallHangingSignRenderers = new Map(WoodTypes.map(type =>
+ [`minecraft:${type}_wall_hanging_sign`, SpecialRenderers.wallHangingSignRenderer(type)]
+ ))
+
+ const DyeColors = [
+ 'white',
+ 'orange',
+ 'magenta',
+ 'light_blue',
+ 'yellow',
+ 'lime',
+ 'pink',
+ 'gray',
+ 'light_gray',
+ 'cyan',
+ 'purple',
+ 'blue',
+ 'brown',
+ 'green',
+ 'red',
+ 'black',
+ ]
+
+ const ShulkerBoxRenderers = new Map(DyeColors.map(color =>
+ [`minecraft:${color}_shulker_box`, SpecialRenderers.shulkerBoxRenderer(Identifier.create(`shulker_${color}`))]
+ ))
+
+ const BedRenderers = new Map(DyeColors.map(color =>
+ [`minecraft:${color}_bed`, SpecialRenderers.bedRenderer(Identifier.create(color))]
+ ))
+
+ const BannerRenderers = new Map(DyeColors.map(color =>
+ [`minecraft:${color}_banner`, SpecialRenderers.bannerRenderer(color)]
+ ))
+
+ const WallBannerRenderers = new Map(DyeColors.map(color =>
+ [`minecraft:${color}_wall_banner`, SpecialRenderers.wallBannerRenderer(color)]
+ ))
-const WallBannerRenderers = new Map(DyeColors.map(color =>
- [`minecraft:${color}_wall_banner`, wallBannerRenderer(color)]
-))
-export namespace SpecialRenderers {
- function getMesh(block: BlockState, atlas: TextureAtlasProvider, cull: Cull): Mesh {
+ export function getBlockMesh(block: BlockState, atlas: TextureAtlasProvider, cull: Cull): Mesh {
if (block.is('water')) {
return liquidRenderer('water', getInt(block, 'level'), atlas, cull, 0)
}
@@ -1001,38 +1006,9 @@ export namespace SpecialRenderers {
if (block.isWaterlogged()) {
mesh.merge(liquidRenderer('water', 0, atlas, cull, 0))
}
- return mesh
- }
- export function getBlockMesh(block: BlockState, atlas: TextureAtlasProvider, cull: Cull): Mesh {
- const mesh = getMesh(block, atlas, cull)
const t = mat4.create()
mat4.scale(t, t, [0.0625, 0.0625, 0.0625])
return mesh.transform(t)
}
-
- export function getItemMesh(item: ItemStack, atlas: TextureAtlasProvider): Mesh {
- if (item.is('shield')) {
- const shieldMesh = shieldRenderer(atlas)
- const t = mat4.create()
- mat4.translate(t, t, [-3, 1, 0])
- mat4.rotateX(t, t, -10 * Math.PI/180)
- mat4.rotateY(t, t, -10 * Math.PI/180)
- mat4.rotateZ(t, t, -5 * Math.PI/180)
- return shieldMesh.transform(t)
- }
- const bedRenderer = BedRenderers.get(item.id.toString())
- if (bedRenderer !== undefined) {
- const headMesh = getMesh(new BlockState(item.id, { part: 'head' }), atlas, Cull.none())
- const footMesh = getMesh(new BlockState(item.id, { part: 'foot' }), atlas, Cull.none())
- const t = mat4.create()
- mat4.translate(t, t, [0, 0, -16])
- return headMesh.merge(footMesh.transform(t))
- }
- if (item.is('bell') || SignRenderers.has(item.id.toString()) || HangingSignRenderers.has(item.id.toString())) {
- return new Mesh()
- }
- // Assumes block and item ID are the same
- return getMesh(new BlockState(item.id), atlas, Cull.none())
- }
}
diff --git a/src/render/index.ts b/src/render/index.ts
index f636bd1..a679b60 100644
--- a/src/render/index.ts
+++ b/src/render/index.ts
@@ -3,7 +3,6 @@ export * from './BlockDefinition.js'
export * from './BlockModel.js'
export * from './ChunkBuilder.js'
export * from './Cull.js'
-export * from './ItemColors.js'
export * from './ItemRenderer.js'
export * from './Line.js'
export * from './Mesh.js'
@@ -15,3 +14,4 @@ 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
new file mode 100644
index 0000000..08ddbd7
--- /dev/null
+++ b/src/util/Color.ts
@@ -0,0 +1,29 @@
+import { NbtTag } from "../nbt/index.js"
+import { Json } from "./Json.js"
+
+export type Color = [number, number, number]
+
+export namespace Color {
+ export function fromJson(obj: unknown): Color | undefined {
+ const packed = Json.readNumber(obj)
+ if (packed) return intToRgb(packed)
+ const array = Json.readArray(obj, o => Json.readNumber(o) ?? 0)
+ if (array === undefined || array.length !== 3) return undefined
+ return array as [number, number, number]
+ }
+
+ export function fromNbt(nbt: NbtTag): Color | undefined {
+ if (nbt.isNumber()) return intToRgb(nbt.getAsNumber())
+ if (!nbt.isListOrArray()) return undefined
+ const values = nbt.getItems()
+ if (values.length < 3) return undefined
+ return values.map(i => i.getAsNumber()) as Color
+ }
+
+ export function intToRgb(n: number): Color {
+ const r = (n >> 16) & 255
+ const g = (n >> 8) & 255
+ const b = n & 255
+ return [r / 255, g / 255, b / 255]
+ }
+}
\ No newline at end of file
diff --git a/src/util/Util.ts b/src/util/Util.ts
index cb6cb05..c2533a3 100644
--- a/src/util/Util.ts
+++ b/src/util/Util.ts
@@ -1,3 +1,4 @@
+
export function lazy(getter: () => T): () => T {
let value: T | null = null
return () => {
@@ -25,12 +26,3 @@ export function mutateWithDefault(map: Map, key: K, initialValue: V,
map.set(key, value)
return value
}
-
-export type Color = [number, number, number]
-
-export function intToRgb(n: number): Color {
- const r = (n >> 16) & 255
- const g = (n >> 8) & 255
- const b = n & 255
- return [r / 255, g / 255, b / 255]
-}
diff --git a/src/util/index.ts b/src/util/index.ts
index b2495f9..ca3c298 100644
--- a/src/util/index.ts
+++ b/src/util/index.ts
@@ -1,3 +1,5 @@
+export * from './Color.js'
export * from './Json.js'
export * from './StringReader.js'
export * from './Util.js'
+
diff --git a/test/render/ItemModel.test.ts b/test/render/ItemModel.test.ts
new file mode 100644
index 0000000..caa6110
--- /dev/null
+++ b/test/render/ItemModel.test.ts
@@ -0,0 +1,301 @@
+import { describe, expect, it, vi } from 'vitest'
+import { BlockModel, Color, Identifier, ItemRendererResources, ItemStack } from '../../src'
+import { ItemModel } from '../../src/render/ItemModel'
+
+describe('ItemModel', () => {
+ const dummyItem = new ItemStack(Identifier.parse('dummy:dummy'), 1)
+
+ const blockModels = {
+ 'test:1': new BlockModel(undefined, undefined, undefined),
+ 'test:2': new BlockModel(undefined, undefined, undefined)
+ }
+
+ const blockModel1 = vi.spyOn(blockModels['test:1'], 'getMesh')
+ const blockModel2 = vi.spyOn(blockModels['test:2'], 'getMesh')
+
+ const resources: ItemRendererResources = {
+ getBlockModel(id) { return blockModels[id.toString()] },
+ getItemModel(id) { return null },
+ getTextureAtlas() { return new ImageData(0, 0) },
+ getTextureUV(texture) { return [0, 0, 0, 0] },
+ }
+
+ it('Model', () => {
+ const model = ItemModel.fromJson({
+ type: 'model',
+ model: 'test:1',
+ tints: [
+ {
+ type: "constant",
+ value: [0.5, 0.6, 0.7]
+ }
+ ]
+ })
+
+ blockModel1.mockClear()
+ model.getMesh(dummyItem, resources, {})
+ expect(blockModel1).toHaveBeenCalledOnce()
+ const tint = blockModel1.mock.calls[0][2]
+ expect(tint).toBeTypeOf('function')
+ expect((tint as (index: number) => Color)(0)).toEqual([0.5, 0.6, 0.7])
+ expect((tint as (index: number) => Color)(1)).toEqual([1, 1, 1])
+ })
+
+ it('Composite', () => {
+ const model = ItemModel.fromJson({
+ type: 'composite',
+ models: [
+ {
+ type: 'model',
+ model: 'test:1',
+ },
+ {
+ type: 'model',
+ model: 'test:1',
+ }
+ ],
+ })
+
+ blockModel1.mockClear()
+ model.getMesh(dummyItem, resources, {})
+ expect(blockModel1).toHaveBeenCalledTimes(2)
+ })
+
+
+ it('Condition', () => {
+ const model = ItemModel.fromJson({
+ type: 'condition',
+ property: 'carried',
+ on_true: {
+ type: 'model',
+ model: 'test:1',
+ },
+ on_false: {
+ type: 'model',
+ model: 'test:2',
+ }
+ })
+
+ blockModel1.mockClear()
+ blockModel2.mockClear()
+ model.getMesh(dummyItem, resources, {carried: true})
+ expect(blockModel1).toHaveBeenCalledOnce()
+ expect(blockModel2).not.toHaveBeenCalled()
+
+ blockModel1.mockClear()
+ blockModel2.mockClear()
+ model.getMesh(dummyItem, resources, {carried: false})
+ expect(blockModel1).not.toHaveBeenCalled()
+ expect(blockModel2).toHaveBeenCalledOnce()
+ })
+
+ it('Condition properties', () => {
+ const fishing_rod_cast = ItemModel.Condition.propertyFromJson({property: 'fishing_rod/cast'})
+ expect(fishing_rod_cast(dummyItem, {'fishing_rod/cast': true})).toBeTruthy()
+ expect(fishing_rod_cast(dummyItem, {'fishing_rod/cast': false})).toBeFalsy()
+
+ const selected = ItemModel.Condition.propertyFromJson({property: 'selected'})
+ expect(selected(dummyItem, {selected: true})).toBeTruthy()
+ expect(selected(dummyItem, {selected: false})).toBeFalsy()
+
+ const carried = ItemModel.Condition.propertyFromJson({property: 'carried'})
+ expect(carried(dummyItem, {carried: true})).toBeTruthy()
+ expect(carried(dummyItem, {carried: false})).toBeFalsy()
+
+ const extended_view = ItemModel.Condition.propertyFromJson({property: 'extended_view'})
+ expect(extended_view(dummyItem, {extended_view: true})).toBeTruthy()
+ expect(extended_view(dummyItem, {extended_view: false})).toBeFalsy()
+
+ const view_entity = ItemModel.Condition.propertyFromJson({property: 'view_entity'})
+ expect(view_entity(dummyItem, {context_entity_is_view_entity: true})).toBeTruthy()
+ expect(view_entity(dummyItem, {context_entity_is_view_entity: false})).toBeFalsy()
+
+ const using_item = ItemModel.Condition.propertyFromJson({property: 'using_item'})
+ expect(using_item(dummyItem, {use_duration: 0})).toBeTruthy()
+ expect(using_item(dummyItem, {use_duration: -1})).toBeFalsy()
+
+ const bundle_has_selected_item = ItemModel.Condition.propertyFromJson({property: 'bundle/has_selected_item'})
+ expect(bundle_has_selected_item(dummyItem, {'bundle/selected_item': 0})).toBeTruthy()
+ expect(bundle_has_selected_item(dummyItem, {'bundle/selected_item': -1})).toBeFalsy()
+
+ const keybind_down = ItemModel.Condition.propertyFromJson({property: 'keybind_down', keybind: 'testkey'})
+ expect(keybind_down(dummyItem, {keybind_down: ['testkey', 'a', 'b']})).toBeTruthy()
+ expect(keybind_down(dummyItem, {keybind_down: ['a', 'b']})).toBeFalsy()
+
+ const broken = ItemModel.Condition.propertyFromJson({property: 'broken'})
+ expect(broken(ItemStack.fromString('dummy:dummy[damage=99,max_damage=100]'), {})).toBeTruthy()
+ expect(broken(ItemStack.fromString('dummy:dummy[damage=98,max_damage=100]'), {})).toBeFalsy()
+ expect(broken(dummyItem, {})).toBeFalsy()
+
+ const damaged = ItemModel.Condition.propertyFromJson({property: 'damaged'})
+ expect(damaged(ItemStack.fromString('dummy:dummy[damage=1,max_damage=100]'), {})).toBeTruthy()
+ expect(damaged(ItemStack.fromString('dummy:dummy[damage=0,max_damage=100]'), {})).toBeFalsy()
+ expect(damaged(dummyItem, {})).toBeFalsy()
+
+ const has_component = ItemModel.Condition.propertyFromJson({property: 'has_component', component: 'glider'})
+ expect(has_component(ItemStack.fromString('dummy:dummy[minecraft:glider={}]'), {})).toBeTruthy()
+ expect(has_component(dummyItem, {})).toBeFalsy()
+
+ const custom_model_data = ItemModel.Condition.propertyFromJson({property: 'custom_model_data', index: 1})
+ expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={flags:[false, true, false]}]'), {})).toBeTruthy()
+ expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={flags:[true, false, true]}]'), {})).toBeFalsy()
+ expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={flags:[true]}]'), {})).toBeFalsy()
+ expect(custom_model_data(dummyItem, {})).toBeFalsy()
+ })
+
+ it('Select', () => {
+ const model = ItemModel.fromJson({
+ type: 'select',
+ property: 'context_entity_type',
+ cases: [
+ {
+ when: 'minecraft:zombie',
+ model: {
+ type: 'model',
+ model: 'test:1',
+ },
+ }
+ ],
+ fallback: {
+ type: 'model',
+ model: 'test:2',
+ }
+ })
+
+ blockModel1.mockClear()
+ blockModel2.mockClear()
+ model.getMesh(dummyItem, resources, {context_entity_type: Identifier.create('zombie')})
+ expect(blockModel1).toHaveBeenCalledOnce()
+ expect(blockModel2).not.toHaveBeenCalled()
+
+ blockModel1.mockClear()
+ blockModel2.mockClear()
+ model.getMesh(dummyItem, resources, {context_entity_type: Identifier.create('skeleton')})
+ expect(blockModel1).not.toHaveBeenCalled()
+ expect(blockModel2).toHaveBeenCalledOnce()
+ })
+
+ 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')
+
+ 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')
+
+ 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, {})).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, {})).toBeNull()
+
+ const charge_type = ItemModel.Select.propertyFromJson({property: 'charge_type'})
+ expect(charge_type(ItemStack.fromString('dummy:dummy[charged_projectiles=[{id:"minecraft:arrow"}, {id:"minecraft:firework_rocket"}]]'), {})).toEqual('rocket')
+ expect(charge_type(ItemStack.fromString('dummy:dummy[charged_projectiles=[{id:"minecraft:arrow"}, {id:"minecraft:arrow"}]]'), {})).toEqual('arrow')
+ expect(charge_type(ItemStack.fromString('dummy:dummy[charged_projectiles=[]]'), {})).toEqual('none')
+ expect(charge_type(dummyItem, {})).toEqual('none')
+
+ const trim_material = ItemModel.Select.propertyFromJson({property: 'trim_material'})
+ expect(trim_material(ItemStack.fromString('dummy:dummy[trim={material:"gold"}]'), {})).toEqual('minecraft:gold')
+ expect(trim_material(dummyItem, {})).toBeNull()
+
+ const block_state = ItemModel.Select.propertyFromJson({property: 'block_state', block_state_property: 'facing'})
+ expect(block_state(ItemStack.fromString('dummy:dummy[block_state={facing: "east"}]'), {})).toEqual('east')
+ expect(block_state(dummyItem, {})).toBeNull()
+
+ const custom_model_data = ItemModel.Select.propertyFromJson({property: 'custom_model_data', index: 1})
+ expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={strings:["a", "b"]}]'), {})).toEqual('b')
+ expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={strings:["a"]}]'), {})).toBeNull()
+ expect(custom_model_data(dummyItem, {})).toBeNull()
+
+ // not testing local_time as it is not implemented
+ })
+
+ it('RangeDisptach', () => {
+ const model = ItemModel.fromJson({
+ type: 'range_dispatch',
+ property: 'use_duration',
+ remaining: false,
+ entries: [
+ {
+ threshold: 2,
+ model: {
+ type: 'model',
+ model: 'test:1',
+ },
+ }
+ ],
+ fallback: {
+ type: 'model',
+ model: 'test:2',
+ }
+ })
+
+ blockModel1.mockClear()
+ blockModel2.mockClear()
+ model.getMesh(dummyItem, resources, {use_duration: 3, max_use_duration:10})
+ expect(blockModel1).toHaveBeenCalledOnce()
+ expect(blockModel2).not.toHaveBeenCalled()
+
+ blockModel1.mockClear()
+ blockModel2.mockClear()
+ model.getMesh(dummyItem, resources, {use_duration: 1, max_use_duration:10})
+ expect(blockModel1).not.toHaveBeenCalled()
+ expect(blockModel2).toHaveBeenCalled()
+ })
+
+ it('RangeDisptach properties', () => {
+ const time_daytime = ItemModel.RangeDispatch.propertyFromJson({property: 'time', source: 'daytime'})
+ expect(time_daytime(dummyItem, {game_time: 6000})).toEqual(0)
+ expect(time_daytime(dummyItem, {game_time: 18000})).toEqual(0.5)
+
+ const time_moon_phase = ItemModel.RangeDispatch.propertyFromJson({property: 'time', source: 'moon_phase'})
+ expect(time_moon_phase(dummyItem, {game_time: 0})).toEqual(0)
+ expect(time_moon_phase(dummyItem, {game_time: 24000})).toEqual(1/8)
+ expect(time_moon_phase(dummyItem, {game_time: 7.5*24000})).toEqual(7.5/8)
+ expect(time_moon_phase(dummyItem, {game_time: 8*24000})).toEqual(0)
+
+ const use_duration_remaining = ItemModel.RangeDispatch.propertyFromJson({property: 'use_duration', remaining: true})
+ expect(use_duration_remaining(dummyItem, {use_duration: 70, max_use_duration: 100})).toEqual(30)
+
+ const use_duration = ItemModel.RangeDispatch.propertyFromJson({property: 'use_duration', remaining: false})
+ expect(use_duration(dummyItem, {use_duration: 70, max_use_duration: 100})).toEqual(70)
+
+ const use_cycle = ItemModel.RangeDispatch.propertyFromJson({property: 'use_cycle', period: 20})
+ expect(use_cycle(dummyItem, {use_duration: 70, max_use_duration: 100})).toEqual(10)
+
+ const damage = ItemModel.RangeDispatch.propertyFromJson({property: 'damage', normalize: false})
+ expect(damage(ItemStack.fromString('dummy:dummy[damage=70,max_damage=100]'), {})).toEqual(70)
+ expect(damage(ItemStack.fromString('dummy:dummy[damage=120,max_damage=100]'), {})).toEqual(100)
+
+ const damage_normalized = ItemModel.RangeDispatch.propertyFromJson({property: 'damage', normalize: true})
+ expect(damage_normalized(ItemStack.fromString('dummy:dummy[damage=70,max_damage=100]'), {})).toEqual(0.7)
+ expect(damage_normalized(ItemStack.fromString('dummy:dummy[damage=120,max_damage=100]'), {})).toEqual(1)
+
+ const count = ItemModel.RangeDispatch.propertyFromJson({property: 'count', normalize: false})
+ const count_normalized = ItemModel.RangeDispatch.propertyFromJson({property: 'count', normalize: true})
+ const itemStack = ItemStack.fromString('dummy:dummy[max_stack_size=64]')
+ expect(count(itemStack, {})).toEqual(1)
+ expect(count_normalized(itemStack, {})).toEqual(1/64)
+ itemStack.count = 10
+ expect(count(itemStack, {})).toEqual(10)
+ expect(count_normalized(itemStack, {})).toEqual(10/64)
+ itemStack.count = 100
+ expect(count(itemStack, {})).toEqual(64)
+ expect(count_normalized(itemStack, {})).toEqual(1)
+
+ const cooldown = ItemModel.RangeDispatch.propertyFromJson({property: 'cooldown'})
+ expect(cooldown(dummyItem, {cooldown_percentage: {'dummy:dummy': 0.7}})).toEqual(0.7)
+ expect(cooldown(ItemStack.fromString('dummy:dummy[use_cooldown={cooldown_group:"test"}]'), {cooldown_percentage: {'minecraft:test': 0.6}})).toEqual(0.6)
+
+ const custom_model_data = ItemModel.RangeDispatch.propertyFromJson({property: 'custom_model_data', index: 1})
+ expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={floats:[0.7, 0.4]}]'), {})).toEqual(0.4)
+ expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={floats:[0.7]}]'), {})).toEqual(0)
+ expect(custom_model_data(dummyItem, {})).toEqual(0)
+
+ // 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
new file mode 100644
index 0000000..95da081
--- /dev/null
+++ b/test/render/ItemTint.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from 'vitest'
+import { Color, ItemStack } from '../../src'
+import { ItemTint } from '../../src/render/ItemTint'
+
+describe('ItemTint', () => {
+ const TEST_COLOR = [0.1, 0.2, 0.3] as Color
+
+ it('fromJson', () => {
+ const constant = ItemTint.fromJson({type: 'constant', value: TEST_COLOR})
+ expect(constant).toBeInstanceOf(ItemTint.Constant)
+ expect((constant as ItemTint.Constant).value).toEqual(TEST_COLOR)
+
+ const dye = ItemTint.fromJson({type: 'dye', default: TEST_COLOR})
+ expect(dye).toBeInstanceOf(ItemTint.Dye)
+ expect((dye as ItemTint.Dye).default_color).toEqual(TEST_COLOR)
+
+ const grass = ItemTint.fromJson({type: 'grass', temperature: 0.7, downfall: 0.4})
+ expect(grass).toBeInstanceOf(ItemTint.Grass)
+ expect((grass as ItemTint.Grass).temperature).toEqual(0.7)
+ expect((grass as ItemTint.Grass).downfall).toEqual(0.4)
+
+ const firework = ItemTint.fromJson({type: 'firework', default: TEST_COLOR})
+ expect(firework).toBeInstanceOf(ItemTint.Firework)
+ expect((firework as ItemTint.Firework).default_color).toEqual(TEST_COLOR)
+
+ const potion = ItemTint.fromJson({type: 'potion', default: TEST_COLOR})
+ expect(potion).toBeInstanceOf(ItemTint.Potion)
+ expect((potion as ItemTint.Potion).default_color).toEqual(TEST_COLOR)
+
+ const map_color = ItemTint.fromJson({type: 'map_color', default: TEST_COLOR})
+ expect(map_color).toBeInstanceOf(ItemTint.MapColor)
+ expect((map_color as ItemTint.MapColor).default_color).toEqual(TEST_COLOR)
+
+ const custom_model_data = ItemTint.fromJson({type: 'custom_model_data', index: 2, default: TEST_COLOR})
+ expect(custom_model_data).toBeInstanceOf(ItemTint.CustomModelData)
+ expect((custom_model_data as ItemTint.CustomModelData).index).toEqual(2)
+ expect((custom_model_data as ItemTint.CustomModelData).default_color).toEqual(TEST_COLOR)
+ })
+
+ it('Constant', () => {
+ expect(new ItemTint.Constant(TEST_COLOR).getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR)
+ })
+
+ it('Dye', () => {
+ const dyeTint = new ItemTint.Dye(TEST_COLOR)
+ expect(dyeTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR)
+ expect(dyeTint.getTint(ItemStack.fromString('dummy:dummy[dyed_color=255]'))).toEqual([0, 0, 1])
+ })
+
+ it('Firework', () => {
+ const fireworkTint = new ItemTint.Firework(TEST_COLOR)
+ expect(fireworkTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR)
+ expect(fireworkTint.getTint(ItemStack.fromString('dummy:dummy[firework_explosion={colors:[255]}]'))).toEqual([0, 0, 1])
+ expect(fireworkTint.getTint(ItemStack.fromString('dummy:dummy[firework_explosion={colors:[255, 0]}]'))).toEqual([0, 0, 0.5])
+ })
+
+ it('Potion', () => {
+ const potionTint = new ItemTint.Potion(TEST_COLOR)
+ expect(potionTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR)
+ expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={custom_color:255}]'))).toEqual([0, 0, 1])
+ expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={potion:"water"}]'))).toEqual(Color.intToRgb(-13083194))
+ expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={potion:"leaping"}]'))).toEqual(Color.intToRgb(16646020))
+ expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={custom_effects:[{id:"jump_boost"}]}]'))).toEqual(Color.intToRgb(16646020))
+ })
+
+ it('MapColor', () => {
+ const mapColorTint = new ItemTint.MapColor(TEST_COLOR)
+ expect(mapColorTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR)
+ expect(mapColorTint.getTint(ItemStack.fromString('dummy:dummy[map_color=255]'))).toEqual([0, 0, 1])
+ })
+
+ it('CustomModelData', () => {
+ const customModelDataTint = new ItemTint.CustomModelData(1, TEST_COLOR)
+ expect(customModelDataTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR)
+ expect(customModelDataTint.getTint(ItemStack.fromString('dummy:dummy[custom_model_data={colors:[[0,0,1]]}]'))).toEqual(TEST_COLOR)
+ expect(customModelDataTint.getTint(ItemStack.fromString('dummy:dummy[custom_model_data={colors:[[0.0,0.0,0.5],[0.0,0.0,1.0]]}]'))).toEqual([0, 0, 1])
+ })
+
+ // not testing grass as its not properly implemented
+})
\ No newline at end of file