diff --git a/js/animations/animation.js b/js/animations/animation.js index 8b55f1ce..0ee4eff0 100644 --- a/js/animations/animation.js +++ b/js/animations/animation.js @@ -588,7 +588,7 @@ class Animation extends AnimationItem { if (undo) { Undo.finishEdit('Remove animation', {animations: []}) - if (isApp && remove_from_file && this.path && fs.existsSync(this.path)) { + if (isApp && Format.animation_files && remove_from_file && this.path && fs.existsSync(this.path)) { Blockbench.showMessageBox({ translateKey: 'delete_animation', icon: 'movie', @@ -792,52 +792,6 @@ class Animation extends AnimationItem { {name: 'menu.animation.loop.loop', icon: animation => (animation.loop == 'loop' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('loop', true)}}, ]}, new MenuSeparator('manage'), - { - name: 'menu.animation.export_java', - id: 'save', - icon: 'save', - condition: () => Format.animation_files, - click(animation) { - - let form = {}; - let keys = []; - let animations = Animation.all.slice() - if (Format.animation_files) animations.sort((a1, a2) => a1.path.hashCode() - a2.path.hashCode()) - animations.forEach(animation => { - let key = animation.name; - keys.push(key) - form[key.hashCode()] = {label: key, type: 'checkbox', value: true}; - }) - let dialog = new Dialog({ - id: 'animation_export', - title: 'dialog.animation_export.title', - form, - onConfirm(form_result) { - dialog.hide(); - keys = keys.filter(key => form_result[key.hashCode()]); - let animations = keys.map(k => Animation.all.find(anim => anim.name == k)); - let content = Codecs.modded_entity.compileAnimations(animations); - Blockbench.export({ - resource_id: 'modded_animation', - type: 'Modded Entity Animation', - extensions: ['java'], - name: (Project.geometry_name||'model'), - content, - }) - } - }) - form.select_all_none = { - type: 'buttons', - buttons: ['generic.select_all', 'generic.select_none'], - click(index) { - let values = {}; - keys.forEach(key => values[key.hashCode()] = (index == 0)); - dialog.setFormValues(values); - } - } - dialog.show(); - } - }, { name: 'menu.animation.save', id: 'save', @@ -2049,6 +2003,7 @@ Interface.definePanels(function() { 'add_animation_controller', 'load_animation_file', 'slider_animation_length', + 'export_modded_animations', ] }) ], diff --git a/js/interface/menu_bar.js b/js/interface/menu_bar.js index b5e0a746..f0ea0600 100644 --- a/js/interface/menu_bar.js +++ b/js/interface/menu_bar.js @@ -181,6 +181,7 @@ const MenuBar = { 'export_obj', 'export_fbx', 'export_collada', + 'export_modded_animations', 'upload_sketchfab', 'share_model', ]}, diff --git a/js/io/formats/modded_entity.js b/js/io/formats/modded_entity.js index 90a6680d..3f41918e 100644 --- a/js/io/formats/modded_entity.js +++ b/js/io/formats/modded_entity.js @@ -280,6 +280,7 @@ const Templates = { ?(has_no_rotation)%(remove_n), PartPose.offset(%(x), %(y), %(z)));`, renderer: `%(bone).render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);`, cube: `.texOffs(%(uv_x), %(uv_y)){?(has_mirror).mirror()}.addBox(%(x), %(y), %(z), %(dx), %(dy), %(dz), new CubeDeformation(%(inflate))){?(has_mirror).mirror(false)}`, + animation_template: 'mojang' }, get(key, version = Project.modded_entity_version) { @@ -295,22 +296,46 @@ const Templates = { } } const AnimationTemplates = { + 'mojang': { + name: 'Mojmaps', + file: + `// Save this class in your mod and generate all required imports + + /** + * Made with Blockbench %(bb_version) + * Exported for Minecraft version 1.19 or later with Mojang mappings + * @author %(author) + */ + public class %(identifier)Animation { + %(animations) + }`, + animation: `public static final AnimationDefinition %(name) = AnimationDefinition.Builder.withLength(%(length))%(looping)%(channels).build();`, + looping: `.looping()`, + channel: `.addAnimation("%(name)", new AnimationChannel(%(channel_type), %(keyframes)))`, + keyframe_rotation: `new Keyframe(%(time), KeyframeAnimations.degreeVec(%(x), %(y), %(z)), %(interpolation))`, + keyframe_position: `new Keyframe(%(time), KeyframeAnimations.posVec(%(x), %(y), %(z)), %(interpolation))`, + keyframe_scale: `new Keyframe(%(time), KeyframeAnimations.scaleVec(%(x), %(y), %(z)), %(interpolation))`, + channel_types: { + rotation: 'AnimationChannel.Targets.ROTATION', + position: 'AnimationChannel.Targets.POSITION', + scale: 'AnimationChannel.Targets.SCALE', + }, + interpolations: { + linear: 'AnimationChannel.Interpolations.LINEAR', + catmullrom: 'AnimationChannel.Interpolations.CATMULLROM', + }, + }, get(key, version = Project.modded_entity_version) { - let temp = Templates[version][key]; + let mapping = Templates.get('animation_template', version); + let temp = AnimationTemplates[mapping || 'mojang'][key]; if (typeof temp === 'string') temp = temp.replace(/\t\t\t/g, ''); return temp; - }, - keepLine(line) { - return line.replace(/\?\(\w+\)/, ''); - }, - getVariableRegex(name) { - return new RegExp(`%\\(${name}\\)`, 'g'); } }; function getIdentifier() { - return (Project.geometry_name && Project.geometry_name.replace(/[\s-]+/g, '_')) || Project.name || 'custom_model'; + return (Project.geometry_name && Project.geometry_name.replace(/[\s-]+/g, '_')) || Project.name || 'CustomModel'; } function askToSaveProject() { @@ -884,14 +909,57 @@ Object.defineProperty(codec, 'remember', { } }) -codec.compileAnimations = function(animations) { +codec.compileAnimations = function(animations = Animation.all) { + let R = Templates.getVariableRegex; + let identifier = getIdentifier(); + let interpolations = AnimationTemplates.get('interpolations'); + let file = AnimationTemplates.get('file'); - - model = model.replace(R('bb_version'), Blockbench.version); - model = model.replace(R('identifier'), identifier); - model = model.replace(R('identifier_rl'), identifier.toLowerCase().replace(' ', '_')); - model = model.replace(R('texture_width'), Project.texture_width); - model = model.replace(R('texture_height'), Project.texture_height); + file = file.replace(R('bb_version'), Blockbench.version); + file = file.replace(R('author'), Settings.get('username') || 'Author'); + file = file.replace(R('identifier'), identifier); + + let anim_strings = []; + animations.forEach(animation => { + let anim_string = AnimationTemplates.get('animation'); + anim_string = anim_string.replace(R('name'), animation.name); + anim_string = anim_string.replace(R('length'), F(animation.length)); + anim_string = anim_string.replace(R('looping'), animation.loop == 'loop' ? AnimationTemplates.get('looping') : ''); + + let channel_strings = []; + let channel_types = AnimationTemplates.get('channel_types'); + for (let id in animation.animators) { + let animator = animation.animators[id]; + if (animator instanceof BoneAnimator == false) continue; + + for (let channel_id in channel_types) { + if (!(animator[channel_id] && animator[channel_id].length)) continue; + let keyframes = animator[channel_id].slice().sort((a, b) => a.time - b.time); + let keyframe_strings = keyframes.map(kf => { + let kf_string = AnimationTemplates.get('keyframe_'+channel_id); + kf_string = kf_string.replace(R('time'), F(kf.time)); + kf_string = kf_string.replace(R('x'), F(kf.calc('x'))); + kf_string = kf_string.replace(R('y'), F(kf.calc('y'))); + kf_string = kf_string.replace(R('z'), F(kf.calc('z'))); + kf_string = kf_string.replace(R('interpolation'), interpolations[kf.interpolation] || interpolations.catmullrom); + return kf_string; + }) + + let channel_string = AnimationTemplates.get('channel'); + channel_string = channel_string.replace(R('name'), animator.name); + channel_string = channel_string.replace(R('channel_type'), channel_types[channel_id]); + channel_string = channel_string.replace(R('keyframes'), '\n\t\t\t' + keyframe_strings.join(',\n\t\t\t') + '\n\t\t'); + + channel_strings.push(channel_string); + } + } + + anim_string = anim_string.replace(R('channels'), '\n\t\t' + channel_strings.join('\n\t\t') + '\n\t\t'); + + anim_strings.push(anim_string); + }) + file = file.replace(R('animations'), anim_strings.join('\n\n\t')); + return file; } var format = new ModelFormat({ @@ -931,6 +999,50 @@ BARS.defineActions(function() { codec.export() } }) + new Action('export_modded_animations', { + icon: 'free_breakfast', + category: 'file', + condition: () => Format == format, + click() { + let form = {}; + let keys = []; + let animations = Animation.all.slice(); + if (Format.animation_files) animations.sort((a1, a2) => a1.path.hashCode() - a2.path.hashCode()); + animations.forEach(animation => { + let key = animation.name; + keys.push(key) + form[key.hashCode()] = {label: key, type: 'checkbox', value: true}; + }) + let dialog = new Dialog({ + id: 'animation_export', + title: 'dialog.animation_export.title', + form, + onConfirm(form_result) { + dialog.hide(); + keys = keys.filter(key => form_result[key.hashCode()]); + let animations = keys.map(k => Animation.all.find(anim => anim.name == k)); + let content = Codecs.modded_entity.compileAnimations(animations); + Blockbench.export({ + resource_id: 'modded_animation', + type: 'Modded Entity Animation', + extensions: ['java'], + name: (Project.geometry_name||'model'), + content, + }) + } + }) + form.select_all_none = { + type: 'buttons', + buttons: ['generic.select_all', 'generic.select_none'], + click(index) { + let values = {}; + keys.forEach(key => values[key.hashCode()] = (index == 0)); + dialog.setFormValues(values); + } + } + dialog.show(); + } + }) }) })() \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 727567b3..0bc50cd9 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1174,6 +1174,8 @@ "action.export_entity.desc": "Export the model as an entity model for Minecraft Bedrock Edition", "action.export_class_entity": "Export Java Entity", "action.export_class_entity.desc": "Export the entity model as a Java class", + "action.export_modded_animations": "Export Modded Entity Animations", + "action.export_modded_animations.desc": "Export animations for a Minecraft Java Edition modded entity model", "action.import_optifine_part": "Import OptiFine Part", "action.import_optifine_part.desc": "Import an entity part model for OptiFine", "action.export_optifine_full": "Export OptiFine JEM",