UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

266 lines (193 loc) 6.32 kB
import LineBuilder from "../../../core/codegen/LineBuilder.js"; import { Behavior } from "./Behavior.js"; /** * * @param {Behavior} behavior * @return {string} */ function compute_behavior_name(behavior) { // try to get name from the constructor field const typeName = behavior.constructor.typeName; if (typeof typeName === "string") { return typeName; } const constructor_name = behavior.constructor.name; if (typeof constructor_name === "string") { return constructor_name; } return '$Behavior'; } /** * * @param {Behavior|SequenceBehavior|ParallelBehavior} behavior * @return {Object} */ function behavior_node_attributes(behavior) { const result = {}; if (behavior.isCompositeBehavior === true) { result.fillcolor = "orange"; if (behavior.isSequenceBehavior === true) { result.fillcolor = "paleturquoise"; } else if (behavior.isParallelBehavior === true) { result.fillcolor = "plum"; } } else { } return result; } /** * * @param {Object} attributes * @return {string} */ function attributes_to_dot(attributes) { const key_names = Object.keys(attributes); return key_names.map(key => { return `${key}=${attributes[key]}`; }).join(', '); } /** * * @param {string} prop * @param {Object} object */ function should_property_be_included(prop, object) { if (prop.startsWith('_')) { // private field return false; } const value = object[prop]; if (object.constructor.prototype[prop] === value) { return false; } const typeof_value = typeof value; if (value === null || typeof_value === "undefined" || typeof_value === "symbol") { return false; } if (typeof_value === "object" && String(value) === '[object Object]') { return false; } return true; } function property_to_string(value, max_length = 200) { let string_value; if (typeof value !== "string") { string_value = String(value); } else { string_value = `\"${value}\"`; } const sanitized = string_value.replace(/\n/, "\n"); if (sanitized.length > max_length) { return '[...]'; } return sanitized; } /** * * @param {Behavior} behavior * @param {number} max_props */ function build_node_properties_string(behavior, max_props = 10) { const properties = {}; let qualified_property_count = 0; for (const behaviorKey in behavior) { if (should_property_be_included(behaviorKey, behavior)) { if (qualified_property_count < max_props) { properties[behaviorKey] = property_to_string(behavior[behaviorKey]); } qualified_property_count++; } } // stringify and concatenate return Object.keys(properties).map(key => `${key} = ${properties[key]}`).join('<BR/>'); } /** * * @param {Behavior} behavior * @return {boolean} */ function should_behavior_have_properties_string(behavior) { if (behavior.isCompositeBehavior === true) { return false; } return true; } function build_node_label(behavior) { const node_name = compute_behavior_name(behavior); const label_pieces = []; label_pieces.push(`<B>${node_name}</B>`); if (should_behavior_have_properties_string(behavior)) { const properties = build_node_properties_string(behavior); if (properties.trim().length > 0) { label_pieces.push(properties); } } return `<{ {${label_pieces.join('<BR/>')} } }>`; } /** * * @param {LineBuilder} out * @param {Behavior|CompositeBehavior|AbstractDecoratorBehavior} behavior * @param {{id_counter:number, node_ids:Map<Behavior,string>}} context * @returns {string} */ function parse_behavior(out, behavior, context) { const id = context.id_counter++; const node_id = `Node_${id}`; context.node_ids.set(behavior, node_id); const attributes = { label: build_node_label(behavior), style: "\"rounded,filled\"" }; Object.assign(attributes, behavior_node_attributes(behavior)); out.add(`${node_id} [${attributes_to_dot(attributes)}]`); if (behavior.isCompositeBehavior === true) { // is a composite behavior, which means it includes other behaviors const children = behavior.children; const child_count = children.length; for (let i = 0; i < child_count; i++) { const child = children[i]; const child_id = parse_behavior(out, child, context); // create edge out.add(`${node_id} -> ${child_id} [label="№${i + 1}"];`); } } if(behavior.isDecoratorBehavior === true){ // is a decorator const source = behavior.getSource(); const id = parse_behavior(out, source,context); out.add(`${node_id} -> ${id} [label="source"];`) } return node_id; } /** * Produces a diagram of behavior tree in DOT format, useful for debugging and exploration of complex trees * * @see https://en.wikipedia.org/wiki/DOT_(graph_description_language) * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 * * @param {Behavior} behavior * @returns {string} */ export function behavior_to_dot(behavior) { const out = new LineBuilder(); const context = { id_counter: 0, node_ids: new Map() }; const font = { name: "helvetica", size: 10 }; out.add("digraph BehaviorTree {"); out.indent(); out.add("graph [style=invis, rankdir=\"TB\" ordering=out, splines=spline]"); out.add(`node [shape=record, fontname=\"${font.name}\", fontsize=${font.size}, margin=\"0.2,0.03\", fillcolor=ivory]`); out.add(`edge [labelfontname=\"${font.name}\", labelfontsize=${font.size}, fontname=\"${font.name}\", fontsize=${font.size}, fontcolor=grey]`); parse_behavior(out, behavior, context); out.dedent(); out.add("}"); return out.build(); }