@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
266 lines (193 loc) • 6.32 kB
JavaScript
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();
}