UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,290 lines 110 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ContentGenerator = void 0; exports.generateContent = generateContent; const CreatorToolsHost_1 = __importDefault(require("../app/CreatorToolsHost")); const ImageCodec_1 = __importDefault(require("../core/ImageCodec")); const PngEncoder_1 = __importDefault(require("./PngEncoder")); const ItemTextureTemplates_1 = require("./ItemTextureTemplates"); const traits_1 = require("./traits"); const ModelDesignUtilities_1 = __importDefault(require("./ModelDesignUtilities")); const TexturedRectangleGenerator_1 = __importDefault(require("./TexturedRectangleGenerator")); const TextureEffects_1 = require("./TextureEffects"); const ModelDesignTemplates_1 = require("./ModelDesignTemplates"); // Initialize trait registry on module load let traitsInitialized = false; function ensureTraitsInitialized() { if (!traitsInitialized) { (0, traits_1.registerAllEntityTraits)(); (0, traits_1.registerAllBlockTraits)(); (0, traits_1.registerAllItemTraits)(); traitsInitialized = true; } } // ============================================================================ // TRAIT EXPANSION // ============================================================================ /** * Trait definitions - maps trait IDs to native Minecraft components. */ const ENTITY_TRAIT_COMPONENTS = { // Body types (primarily affect geometry/animation, but also some components) humanoid: { "minecraft:can_climb": {}, "minecraft:jump.static": {}, }, quadruped: { "minecraft:can_climb": {}, "minecraft:jump.static": {}, }, quadruped_small: { "minecraft:can_climb": {}, "minecraft:jump.static": {}, }, flying: { "minecraft:navigation.fly": { can_path_over_water: true, can_path_over_lava: false, }, "minecraft:can_fly": {}, }, aquatic: { "minecraft:navigation.swim": { can_path_over_water: false, can_swim: true, }, "minecraft:underwater_movement": { value: 0.3 }, "minecraft:breathable": { total_supply: 15, suffocate_time: 0, breathes_water: true, breathes_air: false, }, }, arthropod: { "minecraft:can_climb": {}, "minecraft:mark_variant": { value: 0 }, }, slime: { "minecraft:movement.sway": { sway_amplitude: 0.0 }, }, // Behavior archetypes hostile: { "minecraft:behavior.hurt_by_target": { priority: 1 }, "minecraft:behavior.nearest_attackable_target": { priority: 2, entity_types: [{ filters: { test: "is_family", subject: "other", value: "player" } }], }, "minecraft:attack": { damage: 3 }, }, passive: { "minecraft:behavior.panic": { priority: 1, speed_multiplier: 1.25, }, }, neutral: { "minecraft:behavior.hurt_by_target": { priority: 1, alert_same_type: true, }, }, boss: { "minecraft:boss": { should_darken_sky: true, hud_range: 55, }, }, // Combat styles melee_attacker: { "minecraft:behavior.melee_attack": { priority: 3, speed_multiplier: 1.2, track_target: true, }, "minecraft:attack": { damage: 3 }, }, ranged_attacker: { "minecraft:behavior.ranged_attack": { priority: 3, attack_interval_min: 1.0, attack_interval_max: 3.0, attack_radius: 15.0, }, "minecraft:shooter": { def: "minecraft:arrow", }, }, exploder: { "minecraft:explode": { fuse_length: 1.5, fuse_lit: false, power: 3, causes_fire: false, }, }, // Interaction trader: { "minecraft:trade_table": {}, "minecraft:behavior.trade_with_player": { priority: 1 }, }, tameable: { "minecraft:tameable": { probability: 0.33, tame_items: ["bone"], }, "minecraft:is_tamed": {}, }, rideable: { "minecraft:rideable": { seat_count: 1, family_types: ["player"], interact_text: "action.interact.ride.horse", seats: [{ position: [0.0, 1.1, -0.2] }], }, "minecraft:input_ground_controlled": {}, }, breedable: { "minecraft:breedable": { require_tame: false, breed_items: ["wheat"], breeds_with: { mate_type: "self", baby_type: "self" }, }, "minecraft:behavior.breed": { priority: 3, speed_multiplier: 1.0 }, }, leasable: { "minecraft:leashable": { soft_distance: 4.0, hard_distance: 6.0, max_distance: 10.0, }, }, // Special traits undead: { "minecraft:burns_in_daylight": {}, "minecraft:type_family": { family: ["undead", "monster"] }, }, illager: { "minecraft:type_family": { family: ["illager", "monster"] }, "minecraft:behavior.raid_garden": { priority: 5 }, }, aquatic_only: { "minecraft:breathable": { total_supply: 15, suffocate_time: -1, breathes_water: true, breathes_air: false, generates_bubbles: false, }, }, baby_variant: { "minecraft:is_baby": {}, "minecraft:scale": { value: 0.5 }, "minecraft:ageable": { duration: 1200, grow_up: { event: "minecraft:ageable_grow_up" }, }, }, wanders: { "minecraft:behavior.random_stroll": { priority: 6, speed_multiplier: 1.0 }, "minecraft:behavior.random_look_around": { priority: 7 }, }, patrols: { "minecraft:behavior.move_to_poi": { priority: 3, speed_multiplier: 0.6 }, }, guards: { "minecraft:behavior.defend_village_target": { priority: 1 }, }, flees_daylight: { "minecraft:behavior.flee_sun": { priority: 2, speed_multiplier: 1.0 }, }, teleporter: { "minecraft:teleport": { random_teleports: true, max_random_teleport_time: 30, random_teleport_cube: [32, 16, 32], target_distance: 16, target_teleport_chance: 0.05, }, }, }; /** * Block trait components. */ const BLOCK_TRAIT_COMPONENTS = { solid: {}, transparent: { "minecraft:material_instances": { "*": { render_method: "blend" }, }, }, leaves: { "minecraft:material_instances": { "*": { render_method: "alpha_test" }, }, "minecraft:destructible_by_mining": { seconds_to_destroy: 0.2 }, }, log: { "minecraft:destructible_by_mining": { seconds_to_destroy: 2.0 }, "minecraft:flammable": { catch_chance_modifier: 5, destroy_chance_modifier: 5 }, }, slab: { "minecraft:geometry": "minecraft:geometry.slab", }, stairs: { "minecraft:geometry": "minecraft:geometry.stairs", }, fence: { "minecraft:geometry": { identifier: "minecraft:geometry.fence" }, }, wall: { "minecraft:geometry": { identifier: "minecraft:geometry.wall" }, }, door: { "minecraft:geometry": { identifier: "minecraft:geometry.door" }, "minecraft:on_interact": { event: "toggle_open", }, }, trapdoor: { "minecraft:geometry": { identifier: "minecraft:geometry.trapdoor" }, "minecraft:on_interact": { event: "toggle_open", }, }, workstation: {}, light_source: { "minecraft:light_emission": 15, }, gravity: { "minecraft:gravity": {}, }, liquid: { "minecraft:material_instances": { "*": { render_method: "blend" }, }, }, redstone_signal: { "minecraft:redstone_conductivity": { redstone_conductor: true, allows_wire_to_step_down: true, }, }, redstone_receiver: { "minecraft:redstone_conductivity": { redstone_conductor: true, }, }, button: { "minecraft:geometry": { identifier: "minecraft:geometry.button" }, }, lever: { "minecraft:geometry": { identifier: "minecraft:geometry.lever" }, }, pressure_plate: { "minecraft:geometry": { identifier: "minecraft:geometry.pressure_plate" }, }, flammable: { "minecraft:flammable": { catch_chance_modifier: 5, destroy_chance_modifier: 20 }, }, explosion_resistant: { "minecraft:destructible_by_explosion": { explosion_resistance: 1200 }, }, slippery: { "minecraft:friction": 0.1, }, }; /** * Item trait components. */ const ITEM_TRAIT_COMPONENTS = { sword: { "minecraft:hand_equipped": true, "minecraft:damage": 4, "minecraft:enchantable": { slot: "sword", value: 10 }, }, pickaxe: { "minecraft:hand_equipped": true, "minecraft:digger": { use_efficiency: true, destroy_speeds: [{ block: { tags: "q.any_tag('stone', 'metal')" }, speed: 4 }], }, "minecraft:enchantable": { slot: "pickaxe", value: 10 }, }, axe: { "minecraft:hand_equipped": true, "minecraft:digger": { use_efficiency: true, destroy_speeds: [{ block: { tags: "q.any_tag('wood', 'pumpkin')" }, speed: 4 }], }, "minecraft:enchantable": { slot: "axe", value: 10 }, }, shovel: { "minecraft:hand_equipped": true, "minecraft:digger": { use_efficiency: true, destroy_speeds: [{ block: { tags: "q.any_tag('dirt', 'sand', 'gravel')" }, speed: 4 }], }, "minecraft:enchantable": { slot: "shovel", value: 10 }, }, hoe: { "minecraft:hand_equipped": true, "minecraft:enchantable": { slot: "hoe", value: 10 }, }, bow: { "minecraft:use_duration": 72000, "minecraft:enchantable": { slot: "bow", value: 1 }, }, crossbow: { "minecraft:use_duration": 72000, "minecraft:enchantable": { slot: "crossbow", value: 1 }, }, food: { "minecraft:food": { nutrition: 4, saturation_modifier: "normal", can_always_eat: false, }, "minecraft:use_duration": 32, }, armor_helmet: { "minecraft:wearable": { slot: "slot.armor.head" }, "minecraft:enchantable": { slot: "armor_head", value: 10 }, }, armor_chestplate: { "minecraft:wearable": { slot: "slot.armor.chest" }, "minecraft:enchantable": { slot: "armor_torso", value: 10 }, }, armor_leggings: { "minecraft:wearable": { slot: "slot.armor.legs" }, "minecraft:enchantable": { slot: "armor_legs", value: 10 }, }, armor_boots: { "minecraft:wearable": { slot: "slot.armor.feet" }, "minecraft:enchantable": { slot: "armor_feet", value: 10 }, }, throwable: { "minecraft:throwable": { do_swing_animation: true, launch_power_scale: 1.0, max_launch_power: 1.0, }, "minecraft:projectile": { projectile_entity: "minecraft:snowball", }, }, placeable: { "minecraft:block_placer": { block: "minecraft:stone", }, }, }; // ============================================================================ // BEHAVIOR PRESET EXPANSION // ============================================================================ /** * Maps behavior presets to AI goal components. */ const BEHAVIOR_PRESET_COMPONENTS = { // Movement wander: { "minecraft:behavior.random_stroll": { priority: 6, speed_multiplier: 1.0 }, }, swim: { "minecraft:behavior.random_swim": { priority: 4, speed_multiplier: 1.0 }, }, fly_around: { "minecraft:behavior.random_fly": { priority: 6, xz_dist: 4, y_dist: 2 }, }, float: { "minecraft:behavior.float": { priority: 0 }, }, climb: { "minecraft:can_climb": {}, }, // Combat melee_attack: { "minecraft:behavior.melee_attack": { priority: 3, speed_multiplier: 1.0 }, "minecraft:attack": { damage: 3 }, }, ranged_attack: { "minecraft:behavior.ranged_attack": { priority: 3, attack_radius: 15.0 }, }, target_players: { "minecraft:behavior.nearest_attackable_target": { priority: 2, entity_types: [{ filters: { test: "is_family", subject: "other", value: "player" } }], }, }, target_monsters: { "minecraft:behavior.nearest_attackable_target": { priority: 2, entity_types: [{ filters: { test: "is_family", subject: "other", value: "monster" } }], }, }, flee_when_hurt: { "minecraft:behavior.panic": { priority: 1, speed_multiplier: 1.25 }, }, retaliate: { "minecraft:behavior.hurt_by_target": { priority: 1 }, }, // Social follow_owner: { "minecraft:behavior.follow_owner": { priority: 4, speed_multiplier: 1.0, start_distance: 10, stop_distance: 2, }, }, follow_parent: { "minecraft:behavior.follow_parent": { priority: 5, speed_multiplier: 1.0 }, }, herd: { "minecraft:behavior.move_towards_dwelling_restriction": { priority: 4 }, }, avoid_players: { "minecraft:behavior.avoid_mob_type": { priority: 1, entity_types: [{ filters: { test: "is_family", subject: "other", value: "player" } }], max_dist: 10, walk_speed_multiplier: 0.8, sprint_speed_multiplier: 1.2, }, }, // Interaction look_at_player: { "minecraft:behavior.look_at_player": { priority: 7, look_distance: 6.0, probability: 0.02, }, }, beg: { "minecraft:behavior.beg": { priority: 8, look_distance: 8.0, items: ["bone"], }, }, tempt: { "minecraft:behavior.tempt": { priority: 4, speed_multiplier: 1.0, items: ["wheat"], }, }, sit_command: { "minecraft:behavior.sit": { priority: 2 }, "minecraft:sittable": {}, }, // Actions eat_grass: { "minecraft:behavior.eat_block": { priority: 6, time_until_eat: 1.8, eat_and_replace_block_pairs: [{ eat_block: "grass", replace_block: "dirt" }], }, }, break_doors: { "minecraft:behavior.break_door": { priority: 1 }, }, open_doors: { "minecraft:behavior.open_door": { priority: 6, close_door_after: true }, }, pick_up_items: { "minecraft:behavior.pickup_items": { priority: 7, max_dist: 3 }, }, sleep_in_bed: { "minecraft:behavior.sleep": { priority: 3, speed_multiplier: 1.2 }, }, // Environment hide_from_sun: { "minecraft:behavior.flee_sun": { priority: 2, speed_multiplier: 1.0 }, }, go_home_at_night: { "minecraft:behavior.go_home": { priority: 4, speed_multiplier: 1.0, goal_radius: 1.5 }, }, seek_water: { "minecraft:behavior.go_and_give_items_to_noteblock": { priority: 5 }, }, seek_land: { "minecraft:behavior.move_to_land": { priority: 1, search_range: 16 }, }, }; // ============================================================================ // MAIN GENERATOR CLASS // ============================================================================ /** * Generates native Minecraft content from meta-schema definitions. */ class ContentGenerator { _definition; _options; _namespace; _warnings = []; _errors = []; /** * Sanitizes an ID for safe use in file paths. * Strips path separators and traversal sequences to prevent directory escape. */ static _sanitizeIdForPath(id) { // Remove null bytes, path separators, and traversal sequences return id.replace(/\0/g, "").replace(/\.\./g, "").replace(/[/\\]/g, "_").replace(/:/g, "_"); } constructor(definition) { this._definition = definition; this._options = definition.options || {}; this._namespace = definition.namespace || "custom"; } // ============================================================================ // TRAIT VALIDATION // ============================================================================ /** * Validates trait combinations for entities, blocks, and items. * Checks for conflicting traits and adds warnings to the result. * * @param result - The generation result to add warnings to */ _validateTraitCombinations() { // Validate entity traits if (this._definition.entityTypes) { for (const entity of this._definition.entityTypes) { if (entity.traits && entity.traits.length > 1) { this._validateEntityTraits(entity.id, entity.traits); } } } // Validate block traits if (this._definition.blockTypes) { for (const block of this._definition.blockTypes) { if (block.traits && block.traits.length > 1) { this._validateBlockTraits(block.id, block.traits); } } } // Validate item traits if (this._definition.itemTypes) { for (const item of this._definition.itemTypes) { if (item.traits && item.traits.length > 1) { this._validateItemTraits(item.id, item.traits); } } } } /** * Validates entity trait combinations for conflicts. */ _validateEntityTraits(entityId, traits) { const traitSet = new Set(traits); for (const traitId of traits) { const trait = traits_1.TraitRegistry.getEntityTrait(traitId); if (trait) { const traitData = trait.getData(); // Check for conflicts if (traitData.conflicts) { for (const conflictId of traitData.conflicts) { if (traitSet.has(conflictId)) { this._warnings.push(`Entity '${entityId}': Trait '${traitId}' conflicts with '${conflictId}'. ` + `These traits may produce unexpected behavior when combined.`); } } } } } // Check common known conflicts not defined in trait data this._checkKnownEntityConflicts(entityId, traitSet); } /** * Validates block trait combinations for conflicts. */ _validateBlockTraits(blockId, traits) { const traitSet = new Set(traits); for (const traitId of traits) { const trait = traits_1.TraitRegistry.getBlockTrait(traitId); if (trait) { const traitData = trait.getData(); // Check for conflicts if (traitData.conflicts) { for (const conflictId of traitData.conflicts) { if (traitSet.has(conflictId)) { this._warnings.push(`Block '${blockId}': Trait '${traitId}' conflicts with '${conflictId}'. ` + `These traits may produce unexpected behavior when combined.`); } } } } } // Check common known conflicts this._checkKnownBlockConflicts(blockId, traitSet); } /** * Validates item trait combinations for conflicts. */ _validateItemTraits(itemId, traits) { const traitSet = new Set(traits); for (const traitId of traits) { const trait = traits_1.TraitRegistry.getItemTrait(traitId); if (trait) { const traitData = trait.getData(); // Check for conflicts if (traitData.conflicts) { for (const conflictId of traitData.conflicts) { if (traitSet.has(conflictId)) { this._warnings.push(`Item '${itemId}': Trait '${traitId}' conflicts with '${conflictId}'. ` + `These traits may produce unexpected behavior when combined.`); } } } } } // Check common known conflicts this._checkKnownItemConflicts(itemId, traitSet); } /** * Checks for common entity trait conflicts that may not be defined in trait data. */ _checkKnownEntityConflicts(entityId, traits) { // Behavior conflicts: hostile, passive, neutral are mutually exclusive const behaviorTraits = ["hostile", "passive", "neutral"].filter((t) => traits.has(t)); if (behaviorTraits.length > 1) { this._warnings.push(`Entity '${entityId}': Multiple behavior traits (${behaviorTraits.join(", ")}) are mutually exclusive. ` + `Only one behavior archetype should be used.`); } // Body type conflicts: only one body type should be selected const bodyTypeTraits = [ "humanoid", "quadruped", "quadruped_small", "flying", "aquatic", "arthropod", "slime", ].filter((t) => traits.has(t)); if (bodyTypeTraits.length > 1) { this._warnings.push(`Entity '${entityId}': Multiple body types (${bodyTypeTraits.join(", ")}) specified. ` + `Only one body type should be selected.`); } // aquatic and flying don't mix well if (traits.has("aquatic") && traits.has("flying")) { this._warnings.push(`Entity '${entityId}': 'aquatic' and 'flying' traits may conflict. ` + `Consider using one or the other for cleaner behavior.`); } // undead with passive is unusual if (traits.has("undead") && traits.has("passive")) { this._warnings.push(`Entity '${entityId}': 'undead' trait is typically used with hostile entities, not passive ones.`); } } /** * Checks for common block trait conflicts. */ _checkKnownBlockConflicts(blockId, traits) { // Shape conflicts: only one shape should be selected const shapeTraits = ["slab", "stairs", "fence", "wall", "door", "trapdoor", "button", "lever"].filter((t) => traits.has(t)); if (shapeTraits.length > 1) { this._warnings.push(`Block '${blockId}': Multiple shape traits (${shapeTraits.join(", ")}) specified. ` + `Only one shape type should be selected.`); } // transparent and solid are typically mutually exclusive if (traits.has("transparent") && traits.has("solid")) { this._warnings.push(`Block '${blockId}': 'transparent' and 'solid' traits may be redundant. ` + `Consider which visual style you want.`); } } /** * Checks for common item trait conflicts. */ _checkKnownItemConflicts(itemId, traits) { // Tool conflicts: items should typically be one tool type const toolTraits = ["sword", "pickaxe", "axe", "shovel", "hoe"].filter((t) => traits.has(t)); if (toolTraits.length > 1) { this._warnings.push(`Item '${itemId}': Multiple tool traits (${toolTraits.join(", ")}) specified. ` + `Items are typically one tool type.`); } // Armor conflicts: items should be one armor slot const armorTraits = ["armor_helmet", "armor_chestplate", "armor_leggings", "armor_boots"].filter((t) => traits.has(t)); if (armorTraits.length > 1) { this._warnings.push(`Item '${itemId}': Multiple armor slot traits (${armorTraits.join(", ")}) specified. ` + `Items can only occupy one armor slot.`); } // Food with weapon is unusual if (traits.has("food") && toolTraits.length > 0) { this._warnings.push(`Item '${itemId}': 'food' trait combined with tool trait (${toolTraits.join(", ")}). ` + `This is unusual - consider if this is intentional.`); } } /** * Generate all content from the definition. */ async generate() { // Ensure trait registry is initialized ensureTraitsInitialized(); // Validate trait combinations before generation this._validateTraitCombinations(); const result = { entityBehaviors: [], entityResources: [], blockBehaviors: [], blockResources: [], itemBehaviors: [], itemResources: [], structures: [], features: [], featureRules: [], lootTables: [], recipes: [], spawnRules: [], textures: [], geometries: [], renderControllers: [], sounds: [], summary: { namespace: this._namespace, entityCount: 0, blockCount: 0, itemCount: 0, structureCount: 0, featureCount: 0, lootTableCount: 0, recipeCount: 0, spawnRuleCount: 0, textureCount: 0, warnings: [], errors: [], }, }; // Generate manifests. Resource pack first so the behavior pack can declare // a dependency on it — without this link, Bedrock will not load the RP at // runtime and entities/blocks/items render invisible. result.resourcePackManifest = this._generateResourceManifest(); const rpHeaderUuid = result.resourcePackManifest.content?.header?.uuid; result.behaviorPackManifest = this._generateBehaviorManifest(rpHeaderUuid); // Generate entities if (this._definition.entityTypes) { for (const entity of this._definition.entityTypes) { await this._generateEntity(entity, result); } result.summary.entityCount = this._definition.entityTypes.length; } // Generate blocks if (this._definition.blockTypes) { for (const block of this._definition.blockTypes) { await this._generateBlock(block, result); } result.summary.blockCount = this._definition.blockTypes.length; } // Generate items if (this._definition.itemTypes) { for (const item of this._definition.itemTypes) { await this._generateItem(item, result); } result.summary.itemCount = this._definition.itemTypes.length; } // Generate loot tables if (this._definition.lootTables) { for (const lootTable of this._definition.lootTables) { this._generateLootTable(lootTable, result); } result.summary.lootTableCount = this._definition.lootTables.length; } // Generate recipes if (this._definition.recipes) { for (const recipe of this._definition.recipes) { this._generateRecipe(recipe, result); } result.summary.recipeCount = this._definition.recipes.length; } // Generate spawn rules if (this._definition.spawnRules) { for (const spawnRule of this._definition.spawnRules) { this._generateSpawnRule(spawnRule, result); } result.summary.spawnRuleCount = this._definition.spawnRules.length; } // Generate features if (this._definition.features) { for (const feature of this._definition.features) { this._generateFeature(feature, result); } result.summary.featureCount = this._definition.features.length; } // Generate structures if (this._definition.structures) { for (const structure of this._definition.structures) { await this._generateStructure(structure, result); } result.summary.structureCount = this._definition.structures.length; } // Generate terrain_texture.json for blocks if (this._definition.blockTypes && this._definition.blockTypes.length > 0) { result.terrainTextures = this._generateTerrainTextures(this._definition.blockTypes); result.blocksCatalog = this._generateBlocksCatalog(this._definition.blockTypes); } // Generate item_texture.json for items if (this._definition.itemTypes && this._definition.itemTypes.length > 0) { result.itemTextures = this._generateItemTextures(this._definition.itemTypes); } result.summary.textureCount = result.textures.length; result.summary.warnings = this._warnings; result.summary.errors = this._errors; return result; } // ============================================================================ // MANIFEST GENERATION // ============================================================================ _generateBehaviorManifest(resourcePackHeaderUuid) { const uuid1 = this._generateUuid(); const uuid2 = this._generateUuid(); const content = { format_version: 2, header: { name: this._definition.displayName || `${this._namespace} Behavior Pack`, description: this._definition.description || `Generated content for ${this._namespace}`, uuid: uuid1, version: [1, 0, 0], min_engine_version: [1, 21, 0], }, modules: [ { type: "data", uuid: uuid2, version: [1, 0, 0], }, ], }; // BP must declare a dependency on its sibling RP, otherwise Bedrock won't // load the RP and all entities/blocks render invisible. This was a recurring // root-cause bug that historically required many server restarts to identify. if (resourcePackHeaderUuid) { content.dependencies = [ { uuid: resourcePackHeaderUuid, version: [1, 0, 0], }, ]; } return { path: "manifest.json", pack: "behavior", type: "json", content, }; } _generateResourceManifest() { const uuid1 = this._generateUuid(); const uuid2 = this._generateUuid(); return { path: "manifest.json", pack: "resource", type: "json", content: { format_version: 2, header: { name: this._definition.displayName || `${this._namespace} Resource Pack`, description: this._definition.description || `Resources for ${this._namespace}`, uuid: uuid1, version: [1, 0, 0], min_engine_version: [1, 21, 0], }, modules: [ { type: "resources", uuid: uuid2, version: [1, 0, 0], }, ], }, }; } // ============================================================================ // ENTITY GENERATION // ============================================================================ async _generateEntity(entity, result) { const safeId = ContentGenerator._sanitizeIdForPath(entity.id); const fullId = `${this._namespace}:${entity.id}`; // Build components from traits first let components = { "minecraft:type_family": { family: entity.families || [entity.id] }, "minecraft:collision_box": { width: entity.collisionWidth || 0.6, height: entity.collisionHeight || 1.8, }, "minecraft:physics": {}, "minecraft:pushable": { is_pushable: true, is_pushable_by_piston: true }, "minecraft:movement": { value: entity.movementSpeed || 0.25 }, "minecraft:movement.basic": {}, "minecraft:navigation.walk": { can_path_over_water: true, avoid_damage_blocks: true, }, }; // Collect component groups and events from traits let componentGroups = {}; let events = {}; let spawnEvent = undefined; // Apply traits using the new trait system if (entity.traits) { for (const traitId of entity.traits) { // First try the new registry-based traits const trait = traits_1.TraitRegistry.getEntityTrait(traitId); if (trait) { const traitData = trait.getData({ attackDamage: entity.attackDamage, tameItems: entity.tameItems, tameChance: entity.tameChance, }); // Merge components if (traitData.components) { components = { ...components, ...traitData.components }; } // Merge component groups if (traitData.componentGroups) { componentGroups = { ...componentGroups, ...traitData.componentGroups }; } // Merge events if (traitData.events) { events = { ...events, ...traitData.events }; } // Collect spawn events (last one wins if multiple) if (traitData.spawnEvent) { if (!spawnEvent) { spawnEvent = traitData.spawnEvent; } else { // Merge spawn events - combine component groups to add if (spawnEvent.add?.component_groups && traitData.spawnEvent.add?.component_groups) { spawnEvent.add.component_groups = [ ...spawnEvent.add.component_groups, ...traitData.spawnEvent.add.component_groups, ]; } } } } else { // Fall back to legacy ENTITY_TRAIT_COMPONENTS lookup const traitComponents = ENTITY_TRAIT_COMPONENTS[traitId]; if (traitComponents) { components = { ...components, ...traitComponents }; } } } } // Apply behavior presets if (entity.behaviors) { for (const behavior of entity.behaviors) { const behaviorComponents = BEHAVIOR_PRESET_COMPONENTS[behavior]; if (behaviorComponents) { components = { ...components, ...behaviorComponents }; } } } // Deduplicate movement controllers. Bedrock allows only ONE // `minecraft:movement.*` component per entity; having both `movement.basic` // (added as default above) and a trait-provided controller like // `movement.sway` triggers a "Mobs can only have 1 Move Control Component" // error at runtime. If a trait supplied a more specific movement.* the // default basic controller must be dropped. const movementKeys = Object.keys(components).filter((k) => k.startsWith("minecraft:movement.")); if (movementKeys.length > 1 && components["minecraft:movement.basic"] !== undefined) { delete components["minecraft:movement.basic"]; } // Apply simplified properties if (entity.health !== undefined) { components["minecraft:health"] = { value: entity.health, max: entity.health }; } if (entity.attackDamage !== undefined) { components["minecraft:attack"] = { damage: entity.attackDamage }; } if (entity.followRange !== undefined) { components["minecraft:follow_range"] = { value: entity.followRange }; } if (entity.knockbackResistance !== undefined) { components["minecraft:knockback_resistance"] = { value: entity.knockbackResistance }; } if (entity.scale !== undefined) { components["minecraft:scale"] = { value: entity.scale }; } // appearance.scale overrides entity.scale if both are set (documented in schema). if (entity.appearance?.scale !== undefined) { components["minecraft:scale"] = { value: entity.appearance.scale }; } // Apply native components (override everything) if (entity.components) { components = { ...components, ...entity.components }; } // Generate behavior pack entity const behaviorEntity = { format_version: "1.21.0", "minecraft:entity": { description: { identifier: fullId, is_spawnable: true, is_summonable: true, is_experimental: false, }, components, }, }; // Merge component groups from traits with those specified directly const mergedComponentGroups = { ...componentGroups }; if (entity.componentGroups) { for (const [key, value] of Object.entries(entity.componentGroups)) { mergedComponentGroups[key] = value; } } if (Object.keys(mergedComponentGroups).length > 0) { behaviorEntity["minecraft:entity"].component_groups = mergedComponentGroups; } // Merge events from traits with those specified directly const mergedEvents = { ...events }; if (entity.events) { for (const [key, value] of Object.entries(entity.events)) { mergedEvents[key] = value; } } // Add spawn event if we have one if (spawnEvent) { mergedEvents["minecraft:entity_spawned"] = spawnEvent; } if (Object.keys(mergedEvents).length > 0) { behaviorEntity["minecraft:entity"].events = mergedEvents; } result.entityBehaviors.push({ path: `entities/${safeId}.json`, pack: "behavior", type: "json", content: behaviorEntity, }); // Generate resource pack entity const resourceEntity = this._generateEntityResource(entity, fullId); result.entityResources.push({ path: `entity/${safeId}.entity.json`, pack: "resource", type: "json", content: resourceEntity, }); // Try model-design-based generation for geometry + texture const designResult = await this._generateEntityFromModelDesign(entity); if (designResult) { result.geometries.push({ path: `models/entity/${safeId}.geo.json`, pack: "resource", type: "json", content: designResult.geometry, }); if (designResult.texture) { result.textures.push({ path: `textures/entity/${safeId}.png`, pack: "resource", type: "png", content: designResult.texture, }); } } else { // Fallback: use legacy geometry + placeholder texture const geometry = this._generateEntityGeometry(entity); result.geometries.push({ path: `models/entity/${safeId}.geo.json`, pack: "resource", type: "json", content: geometry, }); const texture = await this._generateEntityTexturePlaceholder(entity); if (texture) { result.textures.push({ path: `textures/entity/${safeId}.png`, pack: "resource", type: "png", content: texture, }); } } // Generate render controller for the entity const renderController = this._generateEntityRenderController(entity); result.renderControllers.push({ path: `render_controllers/${safeId}.render_controllers.json`, pack: "resource", type: "json", content: renderController, }); // Generate loot table from drops if specified if (entity.drops && entity.drops.length > 0) { const lootTable = this._generateLootTableFromDrops(entity.id, entity.drops); result.lootTables.push(lootTable); // Update entity to reference loot table behaviorEntity["minecraft:entity"].components["minecraft:loot"] = { table: `loot_tables/entities/${entity.id}.json`, }; } // Generate spawn rule if specified if (entity.spawning) { const spawnRule = this._generateSpawnRuleFromConfig(entity.id, fullId, entity.spawning); result.spawnRules.push(spawnRule); } } // ============================================================================ // MODEL-DESIGN-BASED ENTITY GENERATION // ============================================================================ /** * Maps a content meta-schema bodyType to a model template type. */ _bodyTypeToTemplateType(bodyType) { switch (bodyType) { case "quadruped": return "large_animal"; case "quadruped_small": return "small_animal"; default: return bodyType; } } /** * Maps an appearance.textureStyle value to the corresponding TexturedRectangleType * used in model-design textures. Falls back to the template's existing background type * (or 'stipple_noise') when no textureStyle is specified. */ _textureStyleToType(textureStyle, fallback) { if (!textureStyle) { return fallback || "stipple_noise"; } switch (textureStyle) { case "solid": return "solid"; case "spotted": return "stipple_noise"; case "striped": return "dither_noise"; case "gradient": return "gradient"; case "organic": return "perlin_noise"; case "armored": // Stippled base + an outset lighting effect is applied by the caller. return "stipple_noise"; default: return fallback || "stipple_noise"; } } /** * Creates a recolored copy of a model design template using the user's primary/secondary colors. * Each named texture in the template gets a distinct color derived from the user's choices, * producing visually distinct faces that make it obvious how to customize the mob. */ _recolorModelDesign(template, primaryColor, secondaryColor, entityId, textureStyle) { const parseHex = (hex) => { const h = hex.startsWith("#") ? hex.slice(1) : hex; return { r: parseInt(h.slice(0, 2), 16) || 128, g: parseInt(h.slice(2, 4), 16) || 128, b: parseInt(h.slice(4, 6), 16) || 128, }; }; const toHex = (c) => { const cl = (v) => Math.max(0, Math.min(255, Math.round(v))); return `#${cl(c.r).toString(16).padStart(2, "0")}${cl(c.g).toString(16).padStart(2, "0")}${cl(c.b).toString(16).padStart(2, "0")}`; }; const primary = parseHex(primaryColor); const secondary = parseHex(secondaryColor); // Generate a palette of distinct colors derived from primary/secondary const colorVariants = [ primary, // variant 0: primary as-is secondary, // variant 1: secondary as-is { r: Math.min(255, primary.r + 40), g: Math.max(0, primary.g - 15), b: Math.max(0, primary.b - 25) }, // variant 2: warmer/brighter { r: Math.max(0, secondary.r - 20), g: Math.max(0, secondary.g - 20), b: Math.min(255, secondary.b + 30) }, // variant 3: cooler/darker { r: Math.min(255, Math.round((primary.r + secondary.r) / 2 + 30)), // variant 4: midtone lighter g: Math.min(255, Math.round((primary.g + secondary.g) / 2 + 30)), b: Math.min(255, Math.round((primary.b + secondary.b) / 2 + 30)), }, { r: Math.max(0, Math.round(primary.r * 0.6)), // variant 5: darkened primary g: Math.max(0, Math.round(primary.g * 0.6)), b: Math.max(0, Math.round(primary.b * 0.6)), }, ]; // Build new textures dict with recolored versions const newTextures = {}; const textureNames = template.textures ? Object.keys(template.textures) : []; for (let i = 0; i < textureNames.length; i++) { const name = textureNames[i]; const original = template.textures[name]; const variant = colorVariants[i % colorVariants.length]; // Create three color variants for noise textures (base, slightly lighter, slightly darker) const c1 = toHex(variant); const c2 = toHex({ r: Math.min(255, variant.r + 15), g: Math.min(255, variant.g + 15), b: Math.min(255, variant.b + 15), }); const c3 = toHex({ r: Math.max(0, variant.r - 15), g: Math.max(0, variant.g - 15), b: Math.max(0, variant.b - 15), }); newTextures[name] = { background: { type: this._textureStyleToType(textureStyle, original.background?.type), colors: [c1, c2, c3], seed: (original.background?.seed || 1000) + i, }, effects: textureStyle === "armored" ? { ...(original.effects || {}), lighting: { preset: "outset", intensity: 0.4 } } : original.effects, pixelArt: original.pixelArt, }; } // Deep clone the design and replace textures const cloned = JSON.parse(JSON.stringify(template)); cloned.textures = newTextures; cloned.identifier = `${this._namespace}_${entityId}`; return cloned; } /** * Generates entity geometry and texture using the model design pipeline. * Uses model templates with per-face textured rectangles for high-quality output. * Returns null if template loading fails, in which case the caller should use legacy generation. */ async _generateEntityFromModelDesign(entity) { const appearance = entity.appearance || {}; const bodyType = appearance.bodyType || "humanoid"; const primaryColor = appearance.primaryColor || "#5B8C3E"; const secondaryColor = appearance.secondaryColor || "#3D6B2E"; // Map bodyType to template type and load template const templateType = this._bodyTypeToTemplateType(bodyType); let template; try { template = await (0, ModelDesignTemplates_1.getModelTemplateAsync)(templateType); } catch { // Template not available - fall back to legacy return null; } if (!template) { return null; } // Recolor the template with the user's colors const design = this._recolorModelDesign(template, primaryColor, secondaryColor, entity.id, appearance.textureStyle); // Convert model design to geometry + atlas regions const conversionResult = ModelDesignUtilities_1.default.convertToGeometry(design); // Override the geometry identifier to use our namespace const geometryId = `geometry.${this._namespace}.${entity.id}`; const geoData = conversionResult.geometry; if (geoData["minecraft:geometry"] && geoData["minecraft:geometry"][0]) { geoData["minecraft:geometry"][0].description.identifier = geometryId; } // Render atlas regions into a pixel buffer (same approach as ImageGenerationUtilities) const [texWidth, texHeight] = conversionResult.textureSize; const pixels = new Uint8Array(texWidth * texHeight * 4); // Initialize with transparent for (let i = 0; i < pixels.length; i += 4) { pixels[i + 3] = 0; } // Render each atlas region for (const region of conversio