UNPKG

bedrock-development

Version:

APIs for creating and editing files related to Minecraft Bedrock development.

564 lines (522 loc) 20.7 kB
import { Directories, getFiles, setFiles } from "../../file_manager.js"; import { mergeDeep, NameData, chalk, currentFormatVersion } from "../../utils.js"; import { MinecraftDataType } from "../minecraft.js"; import { EffectNames, FormatVersion, Identifier, MolangTripleArray } from "../shared_types.js"; import { IServerAnimation, IServerAnimationAnim, ServerAnimation, ServerAnimationName } from "./animation"; import { IServerAC, IServerAnimationController, ServerACName, ServerAnimationController } from "./animation_controller"; import { IServerItemComponents } from "./item"; export type DamageType = string; export type Range = number|[number, number]; export type ServerEntityTarget = 'block'|'damager'|'other'|'parent'|'player'|'self'|'target'; export interface IServerEntity { format_version: FormatVersion; ["minecraft:entity"]: { description: IServerEntityDescription; component_groups?: IServerEntityComponentGroups; components: IServerEntityComponents; events: IServerEntityEvents; } } export interface IServerEntityDescription { identifier: Identifier; runtime_identifier?: Identifier; is_spawnable: boolean; is_summonable: boolean; is_experimental: boolean; properties?: IServerEntityProperties, animations?: { [key: string]: ServerAnimationName|ServerACName; }; scripts?: { animate?: (string|{[key: string]: string})[] } } export interface IServerEntityProperties { [key: Identifier]: { type: 'bool'|'enum'|'int'|'float'; range?: [number, number]; values?: string[]; client_sync?: boolean; default?: boolean|string|number; } } export interface IServerEntityComponentGroups { [key: string]: IServerEntityComponents } export interface IServerEntityComponents { ["minecraft:addrider"]?: { entity_type: string; spawn_event: string; }; ["minecraft:area_attack"]?: { case: DamageType; damage_cooldown?: number; damage_per_tick?: number; damage_range?: number; entity_filter: IServerEntityFilters; play_attack_sound?: boolean; }; ["minecraft:attack"]?: { damage: Range; effect_duration?: number; effect_name?: EffectNames; }; ["minecraft:boss"]?: { hud_range?: number; name: string; should_darken_sky?: boolean; }; ["minecraft:break_blocks"]?: { breakable_blocks: Identifier[]; }; ["minecraft:collision_box"]?: { height?: number; width?: number; }; ["minecraft:custom_hit_test"]?: { hitboxes: { height: number; width: number; pivot: MolangTripleArray; }[]; }; ["minecraft:damage_sensor"]?: { triggers: IServerEntityDamageSensor|IServerEntityDamageSensor[]; }; ["minecraft:despawn"]?: { despawn_from_change?: boolean; despawn_from_distance?: { max_distance?: number; min_distance?: number; }; despawn_from_inactivity?: boolean; despawn_from_simulation_edge?: boolean; filters?: IServerEntityFilters; min_range_inactivity_timer?: number; min_range_random_chance?: number; remove_child_entities?: boolean; }; ["minecraft:environment_sensor"]?: { triggers: IServerEntityTrigger[]; }; ["minecraft:hurt_on_condition"]?: { damage_conditions: { filters: IServerEntityFilters; cause: DamageType; damage_per_tick: number; }[]; }; ["minecraft:instant_despawn"]?: { remove_child_entities?: boolean; }; ["minecraft:interact"]?: { interactions: { interact_text?: string; on_interact?: IServerEntityTrigger; [key: string]: any; } }; ["minecraft:loot"]?: { table: string; }; ["minecraft:physics"]?: { has_collision?: boolean; has_gravity?: boolean; push_towards_closest_space?: boolean; }; ["minecraft:projectile"]?: { on_hit?: { impact_damage?: { damage?: Range; knockback?: boolean; semi_random_diff_damage?: boolean; destroy_on_hit?: boolean; }; stick_in_ground?: { shake_time?: number; }; arrow_effect?: { apply_effect_to_blocking_targets?: boolean; }; definition_event?: { event_trigger: { event: string; target: ServerEntityTarget; } }; }; hit_sound?: string; power: number; gravity: number; uncertainty_base: number; uncertainty_multiplier: number; anchor: number; should_bounce?: boolean; offset: MolangTripleArray; [key: string]: any; }; ["minecraft:pushable"]?: { is_pushable?: boolean; is_pushable_by_piston?: boolean; }; ["minecraft:type_family"]?: { family: string[]; }; [key: string]: any; } export interface IServerEntityFilters { // TODO: Add filter categories test: string; subject?: ServerEntityTarget; operator?: string; domain?: string; value?: string|number|boolean; } export interface IServerEntityDamageSensor { on_damage?: IServerEntityTrigger, deals_damage?: boolean; damage_modifier?: number; damage_multiplier?: number; cause?: DamageType; on_damage_sound_event?: string; } export interface IServerEntityTrigger { filters?: IServerEntityFilters; event?: string; target?: ServerEntityTarget; } export interface IServerEntityEvents { [key: string]: IServerEntityEvent; } export interface IServerEntityEvent { add?: { component_groups: string[]; }; remove?: { component_groups: string[]; }; filters?: IServerEntityFilters; emit_vibration?: { vibration: string; }; queue_command?: { command: string|string[]; }; set_property?: { [key: Identifier]: string|number|boolean; }; trigger?: IServerEntityTrigger|string; randomize?: IServerEntityEvent[]; sequence?: IServerEntityEvent[]; wheight?: number; } export interface IServerEntityComponentGroupOptions { addEvent: boolean; removeEvent: boolean; } export interface IServerEntityPropertiesOptions { createEvents: boolean; } export interface IServerEntityAnimationOptions { createScriptEntry: boolean; } export interface IServerEntityDamageSensorOptions { prepend: boolean; } export class ServerEntity extends MinecraftDataType implements IServerEntity { format_version: FormatVersion; ["minecraft:entity"]: { description: IServerEntityDescription; component_groups?: IServerEntityComponentGroups; components: IServerEntityComponents; events: IServerEntityEvents; } public static get DirectoryPath(): string { return Directories.BEHAVIOR_PATH + 'entities/'; } public get Identifier() : Identifier { return this["minecraft:entity"].description.identifier; } public get NameData() : NameData { return new NameData(this.Identifier); } constructor(filepath: string, template: IServerEntity) { super(filepath, template); this.format_version = template.format_version; this["minecraft:entity"] = template["minecraft:entity"]; } public static createFromTemplate(nameData: NameData): ServerEntity { return new ServerEntity(this.createFilePath(nameData), { format_version: currentFormatVersion, "minecraft:entity": { description: { identifier: nameData.fullname as Identifier, is_spawnable: false, is_summonable: true, is_experimental: false, }, component_groups: { "instant_despawn": { "minecraft:instant_despawn": {} } }, components: { "minecraft:type_family": { family: [ nameData.namespace, nameData.shortname, ] }, "minecraft:collision_box": { height: 0, width: 0, }, "minecraft:physics": { has_collision: false, has_gravity: false, }, "minecraft:damage_sensor": { triggers: [ { on_damage: { filters: { test: "has_damage", value: "void", }, event: "despawn" } }, { cause: "all", deals_damage: false, } ] } }, events: { "despawn": { add: { component_groups: [ "instant_despawn" ] } } } } }); } setComponents(components: IServerItemComponents, handleExisting: 'overwrite'|'merge'|'ignore'='overwrite') { Object.keys(components).forEach(key => { const component = this["minecraft:entity"].components[key]; if (component) { switch (handleExisting) { case 'ignore': console.warn(chalk.yellow(`${this.Identifier} already has component ${key}`)); return; case 'merge': mergeDeep(this["minecraft:entity"].components[key], components[key]); return; case 'overwrite': console.log(chalk.green(`Overwriting existing component ${key} on ${this.Identifier}`)); this["minecraft:entity"].components[key] = components[key]; return; } } console.log(chalk.green(`Added component ${key} to ${this.Identifier}`)); this["minecraft:entity"].components[key] = components[key]; }); } setComponentGroups(groups: IServerEntityComponentGroups, handleExisting: 'overwrite'|'merge'|'ignore'='overwrite', options?: IServerEntityComponentGroupOptions) { this["minecraft:entity"].component_groups = this["minecraft:entity"].component_groups ?? {}; Object.keys(groups).forEach(key => { const group = this["minecraft:entity"].component_groups![key]; if (group) { switch (handleExisting) { case 'ignore': console.warn(chalk.yellow(`${this.Identifier} already has component group ${key}`)); break; case 'merge': mergeDeep(this["minecraft:entity"].component_groups![key], groups[key]); break; case 'overwrite': console.log(chalk.green(`Overwriting existing component group ${key} on ${this.Identifier}`)); this["minecraft:entity"].component_groups![key] = groups[key]; break; } } else { console.log(chalk.green(`Added component group ${key} to ${this.Identifier}`)); this["minecraft:entity"].component_groups![key] = groups[key]; } if (options?.addEvent) { this.setEvents({ [`add_${key}`]: { add: { component_groups: [key] } } }); } if (options?.removeEvent) { this.setEvents({ [`remove_${key}`]: { remove: { component_groups: [key] } } }); } }); } setEvents(events: IServerEntityEvents, handleExisting: 'overwrite'|'merge'|'ignore'='overwrite') { Object.keys(events).forEach(key => { const entityEvents = this["minecraft:entity"].events[key]; if (entityEvents) { switch (handleExisting) { case 'ignore': console.warn(chalk.yellow(`${this.Identifier} already has event ${key}`)); return; case 'merge': mergeDeep(this["minecraft:entity"].events[key], events[key]); break; case 'overwrite': console.log(chalk.green(`Overwriting existing event ${key} on ${this.Identifier}`)); this["minecraft:entity"].events[key] = events[key]; return; } } console.log(chalk.green(`Added event ${key} to ${this.Identifier}`)); this["minecraft:entity"].events[key] = events[key]; }); } setProperties(properties: IServerEntityProperties, handleExisting: 'overwrite'|'merge'|'ignore'='overwrite', options?: IServerEntityPropertiesOptions) { this["minecraft:entity"].description.properties = this["minecraft:entity"].description.properties ?? {}; Object.keys(properties).forEach(key => { const idKey = new NameData(key).fullname as Identifier; const property = this["minecraft:entity"].description.properties![idKey]; if (property) { switch (handleExisting) { case 'ignore': console.warn(chalk.yellow(`${this.Identifier} already has property ${idKey}`)); break; case 'merge': mergeDeep(this["minecraft:entity"].description.properties![idKey], properties[idKey]); case 'overwrite': console.log(chalk.green(`Overwriting existing property ${idKey} on ${this.Identifier}`)); this["minecraft:entity"].description.properties![idKey] = properties[idKey]; break; } } else { console.log(chalk.green(`Added property ${idKey} to ${this.Identifier}`)); this["minecraft:entity"].description.properties![idKey] = properties[idKey]; } if (options?.createEvents) { const keyName = new NameData(idKey); switch (properties[idKey].type) { case "bool": this.setEvents({ [`set_${keyName.shortname}_to_false`]: { set_property: { [idKey]: false } }, [`set_${keyName.shortname}_to_true`]: { set_property: { [idKey]: true } }, }) break; case "enum": const enum_events: IServerEntityEvents = {}; properties[idKey].values?.forEach(value => { enum_events[`set_${keyName.shortname}_to_${value}`] = { set_property: { [idKey]: value } } }); this.setEvents(enum_events); break; case "float": case "int": let start = properties[idKey].range?.[0]; let end = properties[idKey].range?.[1]; if (start !== undefined && end !== undefined) { const number_events: IServerEntityEvents = {}; for (let index = start; index <= end; index++) { number_events[`set_${keyName.shortname}_to_${index}`] = { set_property: { [idKey]: index } } } this.setEvents(number_events); } break; } } }); } setDamageSensor(sensor: IServerEntityDamageSensor, options?: IServerEntityDamageSensorOptions) { this["minecraft:entity"].components["minecraft:damage_sensor"] = this["minecraft:entity"].components["minecraft:damage_sensor"] ?? {triggers: []}; if (!Array.isArray(this["minecraft:entity"].components["minecraft:damage_sensor"].triggers)) { const ogTrigger = this["minecraft:entity"].components["minecraft:damage_sensor"].triggers; this["minecraft:entity"].components["minecraft:damage_sensor"].triggers = [ogTrigger]; } if (options?.prepend) { this["minecraft:entity"].components["minecraft:damage_sensor"].triggers.splice(0, 0, sensor); } else { this["minecraft:entity"].components["minecraft:damage_sensor"].triggers.push(sensor); } } setAnimations(animations: {[key: string]: ServerAnimationName|ServerACName}, handleExisting: 'overwrite'|'merge'|'ignore'='overwrite', options?: IServerEntityAnimationOptions) { this["minecraft:entity"].description.animations = this["minecraft:entity"].description.animations ?? {}; Object.keys(animations).forEach(key => { const entityAnimations = this["minecraft:entity"].description.animations![key]; if (entityAnimations) { switch (handleExisting) { case 'ignore': console.warn(chalk.yellow(`${this.Identifier} already has animation reference ${key}`)); break; case 'merge': mergeDeep(this["minecraft:entity"].description.animations![key], animations[key]); break; case 'overwrite': console.log(chalk.green(`Overwriting existing animation reference ${key} on ${this.Identifier}`)); this["minecraft:entity"].description.animations![key] = animations[key]; break; } } else { console.log(chalk.green(`Added animation reference ${key} to ${this.Identifier}`)); this["minecraft:entity"].description.animations![key] = animations[key]; } if (options?.createScriptEntry) { this.setAnimateScripts(key); } }); } setAnimateScripts(...animations: (string|{[key: string]: string})[]) { this["minecraft:entity"].description.scripts = this["minecraft:entity"].description.scripts ?? {}; this["minecraft:entity"].description.scripts.animate = this["minecraft:entity"].description.scripts.animate ?? []; animations.forEach(animation => { if (!this["minecraft:entity"].description.scripts!.animate!.includes(animation)) { this["minecraft:entity"].description.scripts!.animate!.push(animation); } }); } hasFamilyTypes(...family: string[]): boolean { if (!family.length) return true; return family.every(fType => { if (this["minecraft:entity"].components["minecraft:type_family"]?.family.includes(fType)) { return true; } if (this["minecraft:entity"].component_groups) { for (const key of Object.keys(this["minecraft:entity"].component_groups)) { if (this["minecraft:entity"].component_groups[key]["minecraft:type_family"]?.family.includes(fType)) { return true; } } } return false; }); } }