mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-01 05:45:33 +00:00
Broken graph editor
This commit is contained in:
parent
6215008db7
commit
f996513dca
16 changed files with 1033 additions and 12 deletions
|
|
@ -1,4 +1,4 @@
|
|||
[gd_resource type="Resource" script_class="BossScript" load_steps=5 format=3 uid="uid://1u4y6hvc318e"]
|
||||
[gd_resource type="Resource" script_class="BossScript" format=3 uid="uid://1u4y6hvc318e"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cdd6q2h0t1hhq" path="res://Scripts/Resources/BossPhase.cs" id="1_k3wbt"]
|
||||
[ext_resource type="Resource" uid="uid://cnsmcexyhppdo" path="res://Resources/BossPhases/Rumia/Rumia_NS1_3D.tres" id="2_ovn8y"]
|
||||
|
|
@ -10,3 +10,36 @@ script = ExtResource("2_t8f0y")
|
|||
BossName = &"Rumia"
|
||||
Phases = Array[ExtResource("1_k3wbt")]([ExtResource("2_ovn8y"), ExtResource("3_mi6hp")])
|
||||
metadata/_custom_type_script = "uid://inasa76li3ym"
|
||||
metadata/_bullet_graph_layout_v1 = {
|
||||
"res://Resources/BossPhases/Rumia/Rumia_NS1_3D.tres": Vector2(-2380, 340),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_NS1_3D.tres::Resource_lrsat": Vector2(1140, 490),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_NS1_3D.tres::Resource_mi6hp": Vector2(60, 710),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_NS1_3D.tres::Resource_ovn8y": Vector2(1500, 490),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_NS1_3D.tres::Resource_xfnue": Vector2(-660, -340),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_NS1_3D.tres::legacy_lane": Vector2(-1380, -160),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_NS_1_Chase.tres": Vector2(3380, -580),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres": Vector2(-1400, 900),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_38hmx": Vector2(60, 930),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_46ksb": Vector2(1500, 1150),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_487di": Vector2(420, 930),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_4hlom": Vector2(1140, 710),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_4rhw5": Vector2(780, 930),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_78gkf": Vector2(2820, 2080),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_ali57": Vector2(1500, 710),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_cprus": Vector2(1500, 930),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_ddofg": Vector2(420, 1150),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_x84ys": Vector2(1140, 1150),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::Resource_xmchp": Vector2(1140, 930),
|
||||
"res://Resources/BossPhases/Rumia/Rumia_SP1_3D.tres::legacy_lane": Vector2(780, 710),
|
||||
"res://Resources/BossPhases/Rumia_Boss_Script_3D.tres": Vector2(-3020, 220),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres": Vector2(1800, 1960),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres::Resource_2yamt": Vector2(780, 270),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres::Resource_8xphn": Vector2(-300, 1960),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres::Resource_d0o1f": Vector2(60, 490),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres::Resource_rdwk1": Vector2(420, 270),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres::Resource_t7cg8": Vector2(780, 490),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres::Resource_ye6gf": Vector2(1140, 270),
|
||||
"res://Resources/Patterns/Rumia_NS_2_3D.tres::Resource_yrur2": Vector2(420, 490),
|
||||
"res://Resources/Patterns/Rumia_SP1_Part2.tres": Vector2(60, 1150),
|
||||
"res://Resources/Patterns/Rumia_SP1_Part2_Predicting.tres": Vector2(780, 1150)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,17 @@ burstInterval = 0.1
|
|||
ShotsPerBurst = 4
|
||||
BurstRate = 1.0
|
||||
metadata/_custom_type_script = "uid://bxiprx5nwmpnu"
|
||||
metadata/_bullet_graph_layout_v1 = {
|
||||
"res://Resources/BulletScripts/Danmaku_Room_1.tres::Resource_bdx2k": Vector2(-340, -60)
|
||||
}
|
||||
|
||||
[resource]
|
||||
script = ExtResource("4_7mmfe")
|
||||
Title = "Danmaku Room 1"
|
||||
Description = "Script for the first danmaku room"
|
||||
Patterns = Array[Object]([SubResource("Resource_bdx2k")])
|
||||
metadata/_custom_type_script = "uid://w8hcpu68ssq"
|
||||
metadata/_bullet_graph_layout_v1 = {
|
||||
"res://Resources/BulletScripts/Danmaku_Room_1.tres": Vector2(-1280, 40),
|
||||
"res://Resources/BulletScripts/Danmaku_Room_1.tres::Resource_bdx2k": Vector2(-880, 140)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Cirno.Scripts.Actors;
|
||||
using Cirno.Scripts.AttackPatterns;
|
||||
using Cirno.Scripts.Resources.BulletScripts;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
|
|
@ -12,23 +13,57 @@ public partial class BossPhase : Resource
|
|||
[Export] public string PhaseName = string.Empty;
|
||||
[Export] public int Threshold;
|
||||
[Export] public bool PlayAnimation;
|
||||
[Export] public BulletScript3D BulletScript3D;
|
||||
|
||||
// Legacy path kept for backwards compatibility with existing phase compositions.
|
||||
// TODO: Migrate compositions to BulletScript3D-first workflows where possible.
|
||||
[Export] public Array<AttackPattern> Patterns;
|
||||
|
||||
private int currentPatternIndex = 0;
|
||||
private double patternTimer;
|
||||
private bool _useBulletScript3D;
|
||||
|
||||
private IPatternMachine _patternMachine;
|
||||
|
||||
private BulletScript3D.BulletScriptMachine _bulletScriptMachine;
|
||||
|
||||
public void Start(Node boss)
|
||||
{
|
||||
_useBulletScript3D = BulletScript3D is not null;
|
||||
patternTimer = 0;
|
||||
|
||||
if (_useBulletScript3D)
|
||||
{
|
||||
_bulletScriptMachine = BulletScript3D.Make(boss);
|
||||
_bulletScriptMachine.Start();
|
||||
_patternMachine = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Patterns is null || Patterns.Count == 0)
|
||||
{
|
||||
GD.PushWarning($"BossPhase '{PhaseName}' has no legacy Patterns and no BulletScript3D assigned.");
|
||||
_patternMachine = null;
|
||||
return;
|
||||
}
|
||||
|
||||
currentPatternIndex = 0;
|
||||
_patternMachine = Patterns[currentPatternIndex].MakeMachine(boss);
|
||||
_patternMachine.Start();
|
||||
//Patterns[currentPatternIndex].Start(boss);
|
||||
}
|
||||
|
||||
public void UpdatePhase(double delta)
|
||||
{
|
||||
if (_useBulletScript3D)
|
||||
{
|
||||
_bulletScriptMachine?.UpdatePhase(delta);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_patternMachine is null || Patterns is null || Patterns.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
patternTimer += delta;
|
||||
var currentPattern = Patterns[currentPatternIndex];
|
||||
|
||||
|
|
@ -40,7 +75,6 @@ public partial class BossPhase : Resource
|
|||
var oldParent = _patternMachine.Parent;
|
||||
_patternMachine = Patterns[currentPatternIndex].MakeMachine(oldParent);
|
||||
_patternMachine.Start();
|
||||
//_patternMachine.Start(currentPattern.Parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,22 +9,27 @@ namespace Cirno.Scripts.Resources.BulletScripts;
|
|||
public partial class BulletScript3D : Resource
|
||||
{
|
||||
[Export]
|
||||
public Array<AttackPattern> Patterns { get; private set; }
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
[Export]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[Export]
|
||||
public Array<AttackPattern> Patterns { get; set; } = new Array<AttackPattern>();
|
||||
|
||||
public BulletScriptMachine Make(Node parent)
|
||||
{
|
||||
return new BulletScriptMachine(parent, Patterns);
|
||||
}
|
||||
|
||||
|
||||
public class BulletScriptMachine(Node parent, Array<AttackPattern> patterns)
|
||||
{
|
||||
private int _currentPatternIndex = 0;
|
||||
//private double _patternTimer;
|
||||
|
||||
|
||||
private AttackPattern CurrentPattern => patterns[_currentPatternIndex];
|
||||
|
||||
private IPatternMachine _currentPatternMachine;
|
||||
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (patterns.Count == 0) return;
|
||||
|
|
@ -33,11 +38,11 @@ public partial class BulletScript3D : Resource
|
|||
_currentPatternMachine = CurrentPattern.MakeMachine(parent);
|
||||
_currentPatternMachine.Start();
|
||||
}
|
||||
|
||||
|
||||
public void UpdatePhase(double delta)
|
||||
{
|
||||
//_patternTimer += delta;
|
||||
|
||||
|
||||
_currentPatternMachine.UpdatePattern(delta);
|
||||
//CurrentPattern.UpdatePattern(delta);
|
||||
|
||||
|
|
|
|||
46
addons/bullet_script_graph_editor/inspector_plugin.gd
Normal file
46
addons/bullet_script_graph_editor/inspector_plugin.gd
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
@tool
|
||||
extends EditorInspectorPlugin
|
||||
|
||||
var _plugin: EditorPlugin
|
||||
|
||||
func setup(plugin: EditorPlugin) -> void:
|
||||
_plugin = plugin
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return object is Resource and _is_supported_resource(object)
|
||||
|
||||
func _parse_begin(object: Object) -> void:
|
||||
if not (object is Resource):
|
||||
return
|
||||
|
||||
var button := Button.new()
|
||||
button.text = "Open In Bullet Graph Editor"
|
||||
button.pressed.connect(_on_open_pressed.bind(object))
|
||||
add_custom_control(button)
|
||||
|
||||
func _on_open_pressed(resource: Resource) -> void:
|
||||
if _plugin == null:
|
||||
return
|
||||
_plugin.call("open_resource_in_graph", resource)
|
||||
|
||||
func _is_supported_resource(resource: Resource) -> bool:
|
||||
var script: Script = resource.get_script()
|
||||
if script == null:
|
||||
return false
|
||||
|
||||
var path := script.resource_path
|
||||
if path.contains("/Scripts/AttackPatterns/"):
|
||||
return true
|
||||
if path.contains("/Scripts/Resources/") and path.contains("Pattern"):
|
||||
return true
|
||||
|
||||
if path.ends_with("BossScript.cs") \
|
||||
or path.ends_with("BossPhase.cs") \
|
||||
or path.ends_with("BulletScript3D.cs") \
|
||||
or path.ends_with("BulletScript.cs") \
|
||||
or path.ends_with("AttackPattern.cs") \
|
||||
or path.ends_with("PatternGroup.cs") \
|
||||
or path.ends_with("ParallelPatternGroup.cs"):
|
||||
return true
|
||||
|
||||
return resource.get("WaitForCompletion") != null
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bchuuu8pwh3k
|
||||
281
addons/bullet_script_graph_editor/model/graph_adapter.gd
Normal file
281
addons/bullet_script_graph_editor/model/graph_adapter.gd
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const SUPPORTED_SCRIPT_SUFFIXES := [
|
||||
"BossScript.cs",
|
||||
"BossPhase.cs",
|
||||
"BulletScript3D.cs",
|
||||
"BulletScript.cs",
|
||||
"AttackPattern.cs",
|
||||
"PatternGroup.cs",
|
||||
"ParallelPatternGroup.cs",
|
||||
"MovementPattern.cs"
|
||||
]
|
||||
|
||||
func is_supported_resource(resource: Resource) -> bool:
|
||||
if resource == null:
|
||||
return false
|
||||
var script: Script = resource.get_script()
|
||||
var path := ""
|
||||
if script != null:
|
||||
path = script.resource_path
|
||||
if path != "" and path.contains("/Scripts/AttackPatterns/"):
|
||||
return true
|
||||
if path != "" and path.contains("/Scripts/Resources/") and path.contains("Pattern"):
|
||||
return true
|
||||
for suffix in SUPPORTED_SCRIPT_SUFFIXES:
|
||||
if path != "" and path.ends_with(suffix):
|
||||
return true
|
||||
# Heuristic for AttackPattern-derived resources exposed from C# exports.
|
||||
if resource.get("WaitForCompletion") != null:
|
||||
return true
|
||||
return false
|
||||
|
||||
func build_graph(root: Resource) -> Dictionary:
|
||||
var graph := {
|
||||
"nodes": {},
|
||||
"edges": [],
|
||||
"edge_index": {},
|
||||
"root_id": ""
|
||||
}
|
||||
if root == null:
|
||||
return graph
|
||||
|
||||
var ctx := {
|
||||
"graph": graph,
|
||||
"visited": {}
|
||||
}
|
||||
var root_id := _visit_resource(root, "", "root", "root", ctx)
|
||||
graph.erase("edge_index")
|
||||
graph["root_id"] = root_id
|
||||
return graph
|
||||
|
||||
func _visit_resource(resource: Resource, parent_id: String, edge_type: String, label: String, ctx: Dictionary) -> String:
|
||||
var id := _make_node_id(resource)
|
||||
var nodes: Dictionary = ctx["graph"]["nodes"]
|
||||
|
||||
if not nodes.has(id):
|
||||
nodes[id] = {
|
||||
"id": id,
|
||||
"title": _make_title(resource, label),
|
||||
"subtitle": _make_subtitle(resource),
|
||||
"kind": _get_kind(resource),
|
||||
"resource": resource,
|
||||
"incoming": [],
|
||||
"outgoing": []
|
||||
}
|
||||
|
||||
if parent_id != "":
|
||||
_add_edge(parent_id, id, edge_type, ctx)
|
||||
|
||||
var visited: Dictionary = ctx["visited"]
|
||||
if visited.has(id):
|
||||
return id
|
||||
visited[id] = true
|
||||
|
||||
_process_children(resource, id, ctx)
|
||||
return id
|
||||
|
||||
func _add_edge(from_id: String, to_id: String, edge_type: String, ctx: Dictionary) -> void:
|
||||
var edge_key := "%s|%s|%s" % [from_id, to_id, edge_type]
|
||||
var edge_index: Dictionary = ctx["graph"]["edge_index"]
|
||||
if edge_index.has(edge_key):
|
||||
return
|
||||
edge_index[edge_key] = true
|
||||
ctx["graph"]["edges"].append({
|
||||
"from": from_id,
|
||||
"to": to_id,
|
||||
"type": edge_type
|
||||
})
|
||||
|
||||
var nodes: Dictionary = ctx["graph"]["nodes"]
|
||||
if nodes.has(from_id):
|
||||
nodes[from_id]["outgoing"].append(edge_type)
|
||||
if nodes.has(to_id):
|
||||
nodes[to_id]["incoming"].append(edge_type)
|
||||
|
||||
func _ensure_virtual_node(id: String, title: String, subtitle: String, kind: String, ctx: Dictionary) -> void:
|
||||
var nodes: Dictionary = ctx["graph"]["nodes"]
|
||||
if nodes.has(id):
|
||||
return
|
||||
nodes[id] = {
|
||||
"id": id,
|
||||
"title": title,
|
||||
"subtitle": subtitle,
|
||||
"kind": kind,
|
||||
"resource": null,
|
||||
"incoming": [],
|
||||
"outgoing": []
|
||||
}
|
||||
|
||||
func _process_children(resource: Resource, id: String, ctx: Dictionary) -> void:
|
||||
var kind := _get_kind(resource)
|
||||
|
||||
if kind == "BossScript":
|
||||
_visit_ordered_array(resource, "Phases", id, "phase", "phase", ctx)
|
||||
return
|
||||
|
||||
if kind == "BossPhase":
|
||||
var bullet_lane_id := "%s::bullet_lane" % id
|
||||
var legacy_lane_id := "%s::legacy_lane" % id
|
||||
|
||||
var bullet_script: Variant = _get_prop(resource, "BulletScript3D")
|
||||
if bullet_script is Resource:
|
||||
_ensure_virtual_node(bullet_lane_id, "BulletScript3D Lane", "Preferred execution path", "Lane", ctx)
|
||||
_add_edge(id, bullet_lane_id, "bullet_script_lane", ctx)
|
||||
_visit_resource(bullet_script, bullet_lane_id, "bullet_script", "BulletScript3D", ctx)
|
||||
|
||||
var legacy_values: Array = _get_array_prop(resource, "Patterns")
|
||||
if legacy_values.size() > 0:
|
||||
_ensure_virtual_node(legacy_lane_id, "Legacy Patterns Lane", "Compatibility path", "LegacyLane", ctx)
|
||||
_add_edge(id, legacy_lane_id, "legacy_lane", ctx)
|
||||
_visit_ordered_array(resource, "Patterns", legacy_lane_id, "legacy", "pattern", ctx)
|
||||
return
|
||||
|
||||
if kind == "BulletScript3D" or kind == "BulletScript":
|
||||
_visit_ordered_array(resource, "Patterns", id, "sequence", "pattern", ctx)
|
||||
return
|
||||
|
||||
if kind == "PatternGroup":
|
||||
_visit_ordered_array_fallback(resource, ["Patterns", "patterns"], id, "group", "child", ctx)
|
||||
return
|
||||
|
||||
if kind == "ParallelPatternGroup":
|
||||
_visit_parallel_array(resource, "Patterns", id, "parallel", "child", ctx)
|
||||
return
|
||||
|
||||
if kind == "MovementPattern":
|
||||
var child: Variant = _get_prop(resource, "shootingPattern")
|
||||
if child is Resource:
|
||||
_visit_resource(child, id, "embedded", "shootingPattern", ctx)
|
||||
return
|
||||
|
||||
_process_common_links(resource, id, ctx)
|
||||
|
||||
func _process_common_links(resource: Resource, id: String, ctx: Dictionary) -> void:
|
||||
for property_name in ["BulletScript3D", "BulletScript", "Script", "shootingPattern"]:
|
||||
var linked: Variant = _get_prop(resource, property_name)
|
||||
if linked is Resource:
|
||||
_visit_resource(linked, id, "reference", property_name, ctx)
|
||||
|
||||
# Generic fallback for pattern arrays in custom pattern resources.
|
||||
for array_name in ["Patterns", "patterns"]:
|
||||
var values := _get_array_prop(resource, array_name)
|
||||
if values.size() > 0:
|
||||
_visit_ordered_array_fallback(resource, [array_name], id, "sequence", "pattern", ctx)
|
||||
return
|
||||
|
||||
func _visit_ordered_array(owner: Resource, property_name: String, parent_id: String, first_edge_type: String, label_prefix: String, ctx: Dictionary) -> void:
|
||||
var values: Array = _get_array_prop(owner, property_name)
|
||||
var prev := parent_id
|
||||
for i in values.size():
|
||||
var item = values[i]
|
||||
if not (item is Resource):
|
||||
continue
|
||||
var edge_type := first_edge_type if prev == parent_id else "sequence"
|
||||
var label := "%s[%d]" % [label_prefix, i]
|
||||
prev = _visit_resource(item, prev, edge_type, label, ctx)
|
||||
|
||||
func _visit_ordered_array_fallback(owner: Resource, property_names: Array, parent_id: String, first_edge_type: String, label_prefix: String, ctx: Dictionary) -> void:
|
||||
for property_name in property_names:
|
||||
var values: Array = _get_array_prop(owner, property_name)
|
||||
if values.size() == 0:
|
||||
continue
|
||||
var prev := parent_id
|
||||
for i in values.size():
|
||||
var item = values[i]
|
||||
if not (item is Resource):
|
||||
continue
|
||||
var edge_type := first_edge_type if prev == parent_id else "sequence"
|
||||
var label := "%s[%d]" % [label_prefix, i]
|
||||
prev = _visit_resource(item, prev, edge_type, label, ctx)
|
||||
return
|
||||
|
||||
func _visit_parallel_array(owner: Resource, property_name: String, parent_id: String, edge_type: String, label_prefix: String, ctx: Dictionary) -> void:
|
||||
var values: Array = _get_array_prop(owner, property_name)
|
||||
for i in values.size():
|
||||
var item = values[i]
|
||||
if not (item is Resource):
|
||||
continue
|
||||
var label := "%s[%d]" % [label_prefix, i]
|
||||
_visit_resource(item, parent_id, edge_type, label, ctx)
|
||||
|
||||
func _get_array_prop(resource: Resource, property_name: String) -> Array:
|
||||
var value = _get_prop(resource, property_name)
|
||||
return value if value is Array else []
|
||||
|
||||
func _get_prop(resource: Resource, property_name: String):
|
||||
if resource == null:
|
||||
return null
|
||||
if not _has_property(resource, property_name):
|
||||
return null
|
||||
if resource.has_method("get"):
|
||||
return resource.get(property_name)
|
||||
return null
|
||||
|
||||
func _has_property(resource: Object, property_name: String) -> bool:
|
||||
for property_data in resource.get_property_list():
|
||||
if property_data is Dictionary and property_data.get("name", "") == property_name:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _make_node_id(resource: Resource) -> String:
|
||||
if resource.resource_path != "":
|
||||
return resource.resource_path
|
||||
return "%s:%s" % [_get_kind(resource), str(resource.get_instance_id())]
|
||||
|
||||
func _make_title(resource: Resource, label: String) -> String:
|
||||
var kind := _get_kind(resource)
|
||||
# Prefer explicit Title property if present
|
||||
var title_prop = _get_prop(resource, "Title")
|
||||
if title_prop != null and str(title_prop) != "":
|
||||
return "%s" % str(title_prop)
|
||||
|
||||
if kind == "BossScript":
|
||||
var boss_name = _get_prop(resource, "BossName")
|
||||
if boss_name != null and str(boss_name) != "":
|
||||
return "BossScript: %s" % str(boss_name)
|
||||
if kind == "BossPhase":
|
||||
var phase_name = _get_prop(resource, "PhaseName")
|
||||
if phase_name != null and str(phase_name) != "":
|
||||
return "BossPhase: %s" % str(phase_name)
|
||||
if label != "" and label != "root":
|
||||
return "%s (%s)" % [kind, label]
|
||||
return kind
|
||||
|
||||
func _make_subtitle(resource: Resource) -> String:
|
||||
# Prefer Description property when available
|
||||
var desc = _get_prop(resource, "Description")
|
||||
if desc != null and str(desc) != "":
|
||||
return str(desc)
|
||||
if resource.resource_path != "":
|
||||
return resource.resource_path
|
||||
return "subresource"
|
||||
|
||||
func _get_kind(resource: Resource) -> String:
|
||||
var script: Script = resource.get_script()
|
||||
if script == null:
|
||||
return resource.get_class()
|
||||
|
||||
var path := script.resource_path
|
||||
if path.ends_with("BossScript.cs"):
|
||||
return "BossScript"
|
||||
if path.ends_with("BossPhase.cs"):
|
||||
return "BossPhase"
|
||||
if path.ends_with("BulletScript3D.cs"):
|
||||
return "BulletScript3D"
|
||||
if path.ends_with("BulletScript.cs"):
|
||||
return "BulletScript"
|
||||
if path.ends_with("PatternGroup.cs"):
|
||||
return "PatternGroup"
|
||||
if path.ends_with("ParallelPatternGroup.cs"):
|
||||
return "ParallelPatternGroup"
|
||||
if path.ends_with("MovementPattern.cs"):
|
||||
return "MovementPattern"
|
||||
if path.ends_with("AttackPattern.cs"):
|
||||
return "AttackPattern"
|
||||
if path.contains("/Scripts/AttackPatterns/"):
|
||||
return path.get_file().trim_suffix(".cs")
|
||||
|
||||
var file_name := path.get_file().trim_suffix(".cs")
|
||||
return file_name if file_name != "" else resource.get_class()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dkea0407rg2ar
|
||||
7
addons/bullet_script_graph_editor/plugin.cfg
Normal file
7
addons/bullet_script_graph_editor/plugin.cfg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Bullet Script Graph Editor"
|
||||
description="Graph editor for BossScript and BulletScript resources."
|
||||
author="GitHub Copilot"
|
||||
version="0.1.0"
|
||||
script="plugin.gd"
|
||||
32
addons/bullet_script_graph_editor/plugin.gd
Normal file
32
addons/bullet_script_graph_editor/plugin.gd
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const GraphEditorDockScene := preload("res://addons/bullet_script_graph_editor/ui/graph_editor_dock.tscn")
|
||||
const InspectorPluginScript := preload("res://addons/bullet_script_graph_editor/inspector_plugin.gd")
|
||||
|
||||
var _dock_instance: PanelContainer
|
||||
var _inspector_plugin: EditorInspectorPlugin
|
||||
|
||||
func _enter_tree() -> void:
|
||||
_dock_instance = GraphEditorDockScene.instantiate()
|
||||
_dock_instance.setup(get_editor_interface())
|
||||
add_control_to_bottom_panel(_dock_instance, "Bullet Graph")
|
||||
|
||||
_inspector_plugin = InspectorPluginScript.new()
|
||||
_inspector_plugin.setup(self)
|
||||
add_inspector_plugin(_inspector_plugin)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _inspector_plugin:
|
||||
remove_inspector_plugin(_inspector_plugin)
|
||||
_inspector_plugin = null
|
||||
|
||||
if _dock_instance:
|
||||
remove_control_from_bottom_panel(_dock_instance)
|
||||
_dock_instance.queue_free()
|
||||
_dock_instance = null
|
||||
|
||||
func open_resource_in_graph(resource: Resource) -> void:
|
||||
if _dock_instance == null:
|
||||
return
|
||||
_dock_instance.open_resource(resource)
|
||||
1
addons/bullet_script_graph_editor/plugin.gd.uid
Normal file
1
addons/bullet_script_graph_editor/plugin.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://q2ctk5gbyvve
|
||||
499
addons/bullet_script_graph_editor/ui/graph_editor_dock.gd
Normal file
499
addons/bullet_script_graph_editor/ui/graph_editor_dock.gd
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
@tool
|
||||
extends PanelContainer
|
||||
|
||||
const GraphAdapterScript := preload("res://addons/bullet_script_graph_editor/model/graph_adapter.gd")
|
||||
const META_LAYOUT_KEY := "_bullet_graph_layout_v1"
|
||||
|
||||
var _editor_interface: EditorInterface
|
||||
var _graph_adapter: RefCounted
|
||||
var _current_resource: Resource
|
||||
|
||||
var _path_edit: LineEdit
|
||||
var _status_label: Label
|
||||
var _graph_edit: GraphEdit
|
||||
var _file_dialog: FileDialog
|
||||
var _open_selected_button: Button
|
||||
var _inspect_selected_button: Button
|
||||
var _selected_node_resource: Resource
|
||||
var _wait_checkbox: CheckBox
|
||||
|
||||
func setup(editor_interface: EditorInterface) -> void:
|
||||
_editor_interface = editor_interface
|
||||
|
||||
func _ready() -> void:
|
||||
_graph_adapter = GraphAdapterScript.new()
|
||||
_build_ui()
|
||||
|
||||
func is_supported_resource(resource: Resource) -> bool:
|
||||
if _graph_adapter == null:
|
||||
return false
|
||||
return _graph_adapter.is_supported_resource(resource)
|
||||
|
||||
func open_resource(resource: Resource) -> void:
|
||||
if resource == null:
|
||||
_set_status("No resource selected.", true)
|
||||
return
|
||||
|
||||
if not is_supported_resource(resource):
|
||||
_set_status("Unsupported resource: %s" % resource.resource_path, true)
|
||||
return
|
||||
# Build the graph first to detect whether any nodes will be generated (diagnostic)
|
||||
var built_graph: Dictionary = _graph_adapter.build_graph(resource)
|
||||
var built_nodes: Dictionary = built_graph.get("nodes", {})
|
||||
_current_resource = resource
|
||||
_path_edit.text = resource.resource_path
|
||||
_render_graph()
|
||||
if built_nodes.size() == 0:
|
||||
_set_status("Opened: %s (no nodes detected)" % _display_path(resource), true)
|
||||
else:
|
||||
_set_status("Opened: %s" % _display_path(resource), false)
|
||||
|
||||
func _build_ui() -> void:
|
||||
var root := VBoxContainer.new()
|
||||
root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
root.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(root)
|
||||
|
||||
var toolbar := HBoxContainer.new()
|
||||
root.add_child(toolbar)
|
||||
|
||||
var open_button := Button.new()
|
||||
open_button.text = "Open Resource"
|
||||
open_button.pressed.connect(_on_open_pressed)
|
||||
toolbar.add_child(open_button)
|
||||
|
||||
var open_inspected_button := Button.new()
|
||||
open_inspected_button.text = "Open Inspected"
|
||||
open_inspected_button.pressed.connect(_on_open_inspected_pressed)
|
||||
toolbar.add_child(open_inspected_button)
|
||||
|
||||
var save_layout_button := Button.new()
|
||||
save_layout_button.text = "Save Layout"
|
||||
save_layout_button.pressed.connect(_save_layout_metadata)
|
||||
toolbar.add_child(save_layout_button)
|
||||
|
||||
_open_selected_button = Button.new()
|
||||
_open_selected_button.text = "Open Selected Node"
|
||||
_open_selected_button.disabled = true
|
||||
_open_selected_button.pressed.connect(_on_open_selected_node_pressed)
|
||||
toolbar.add_child(_open_selected_button)
|
||||
|
||||
_inspect_selected_button = Button.new()
|
||||
_inspect_selected_button.text = "Inspect Selected Node"
|
||||
_inspect_selected_button.disabled = true
|
||||
_inspect_selected_button.pressed.connect(_on_inspect_selected_node_pressed)
|
||||
toolbar.add_child(_inspect_selected_button)
|
||||
|
||||
_path_edit = LineEdit.new()
|
||||
_path_edit.placeholder_text = "res://Resources/..."
|
||||
_path_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_path_edit.text_submitted.connect(_on_path_submitted)
|
||||
toolbar.add_child(_path_edit)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.text = "Open a BossScript/BossPhase/BulletScript to render graph."
|
||||
root.add_child(_status_label)
|
||||
|
||||
_wait_checkbox = CheckBox.new()
|
||||
_wait_checkbox.text = "Wait For Completion (selected pattern)"
|
||||
_wait_checkbox.visible = false
|
||||
_wait_checkbox.toggled.connect(_on_wait_for_completion_toggled)
|
||||
root.add_child(_wait_checkbox)
|
||||
|
||||
_graph_edit = GraphEdit.new()
|
||||
_graph_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_graph_edit.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_graph_edit.right_disconnects = true
|
||||
_graph_edit.end_node_move.connect(_save_layout_metadata)
|
||||
_graph_edit.node_selected.connect(_on_graph_node_selected)
|
||||
root.add_child(_graph_edit)
|
||||
|
||||
_file_dialog = FileDialog.new()
|
||||
_file_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_file_dialog.add_filter("*.tres; Godot Resource")
|
||||
_file_dialog.add_filter("*.res; Godot Resource")
|
||||
_file_dialog.file_selected.connect(_on_file_selected)
|
||||
add_child(_file_dialog)
|
||||
|
||||
func _on_open_pressed() -> void:
|
||||
_file_dialog.popup_centered_ratio(0.8)
|
||||
|
||||
func _on_open_inspected_pressed() -> void:
|
||||
if _editor_interface == null:
|
||||
_set_status("Editor interface is not available.", true)
|
||||
return
|
||||
var inspector := _editor_interface.get_inspector()
|
||||
if inspector == null:
|
||||
_set_status("Inspector is not available.", true)
|
||||
return
|
||||
|
||||
var edited_object := inspector.get_edited_object()
|
||||
if not (edited_object is Resource):
|
||||
_set_status("Inspector object is not a supported resource.", true)
|
||||
return
|
||||
|
||||
open_resource(edited_object)
|
||||
|
||||
func _on_file_selected(path: String) -> void:
|
||||
var loaded = ResourceLoader.load(path)
|
||||
if not (loaded is Resource):
|
||||
_set_status("Failed to load resource: %s" % path, true)
|
||||
return
|
||||
open_resource(loaded)
|
||||
|
||||
func _on_path_submitted(path: String) -> void:
|
||||
if path.strip_edges() == "":
|
||||
return
|
||||
_on_file_selected(path)
|
||||
|
||||
func _clear_graph() -> void:
|
||||
for child in _graph_edit.get_children():
|
||||
if child is GraphNode:
|
||||
child.queue_free()
|
||||
|
||||
func _render_graph() -> void:
|
||||
if _current_resource == null:
|
||||
return
|
||||
|
||||
_clear_graph()
|
||||
|
||||
var graph: Dictionary = _graph_adapter.build_graph(_current_resource)
|
||||
var nodes: Dictionary = graph.get("nodes", {})
|
||||
var edges: Array = graph.get("edges", [])
|
||||
var node_name_by_id := {}
|
||||
_selected_node_resource = null
|
||||
_open_selected_button.disabled = true
|
||||
_inspect_selected_button.disabled = true
|
||||
_wait_checkbox.visible = false
|
||||
|
||||
var layout := _load_layout_metadata()
|
||||
var auto_index := 0
|
||||
|
||||
for node_id in nodes.keys():
|
||||
var node_data: Dictionary = nodes[node_id]
|
||||
var graph_node := GraphNode.new()
|
||||
graph_node.name = _node_name_from_id(node_id)
|
||||
graph_node.title = node_data.get("title", "Node")
|
||||
graph_node.resizable = false
|
||||
graph_node.custom_minimum_size = Vector2(240, 84)
|
||||
graph_node.set_meta("graph_id", node_id)
|
||||
|
||||
var info := VBoxContainer.new()
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = node_data.get("subtitle", "")
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
subtitle.clip_text = true
|
||||
subtitle.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
info.add_child(subtitle)
|
||||
|
||||
# Collapsible details handled below when there are actual details to show
|
||||
|
||||
# Determine if there are details to show (description or exported properties or patterns add button)
|
||||
var potential_details := []
|
||||
var node_resource: Resource = node_data.get("resource", null)
|
||||
if node_resource != null:
|
||||
graph_node.set_meta("resource", node_resource)
|
||||
if node_resource != null:
|
||||
var desc_text := str(node_data.get("subtitle", ""))
|
||||
if desc_text != "":
|
||||
potential_details.append("desc")
|
||||
# check for exported properties (editor usage)
|
||||
for property_data in node_resource.get_property_list():
|
||||
if property_data is Dictionary:
|
||||
var pname := property_data.get("name", "")
|
||||
if pname == "" or pname.begins_with("_") or pname == "Patterns":
|
||||
continue
|
||||
var usage := int(property_data.get("usage", 0))
|
||||
if usage & PROPERTY_USAGE_EDITOR:
|
||||
potential_details.append(pname)
|
||||
# Add Pattern button is considered a detail for BulletScript3D
|
||||
if node_data.get("kind", "") == "BulletScript3D":
|
||||
potential_details.append("add_pattern")
|
||||
|
||||
var has_details := potential_details.size() > 0
|
||||
var details: VBoxContainer = null
|
||||
var toggle: Button = null
|
||||
if has_details:
|
||||
details = VBoxContainer.new()
|
||||
details.name = "details"
|
||||
details.visible = false
|
||||
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
info.add_child(details)
|
||||
|
||||
# Toggle button to expand/collapse
|
||||
toggle = Button.new()
|
||||
toggle.text = "Details"
|
||||
toggle.toggle_mode = true
|
||||
# default is not pressed; use setter to avoid assigning to a constant
|
||||
toggle.set_pressed(false)
|
||||
# bind graph_node and details so we can resize the node when toggling
|
||||
toggle.pressed.connect(Callable(self, "_on_toggle_details_pressed").bind(graph_node, details))
|
||||
info.add_child(toggle)
|
||||
|
||||
|
||||
graph_node.add_child(info)
|
||||
|
||||
# Add quick action buttons on each node for convenience
|
||||
if node_resource != null:
|
||||
var actions := HBoxContainer.new()
|
||||
actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var btn_open := Button.new()
|
||||
btn_open.text = "Open"
|
||||
btn_open.pressed.connect(Callable(self, "_on_node_open_pressed").bind(graph_node))
|
||||
actions.add_child(btn_open)
|
||||
|
||||
var btn_inspect := Button.new()
|
||||
btn_inspect.text = "Inspect"
|
||||
btn_inspect.pressed.connect(Callable(self, "_on_node_inspect_pressed").bind(graph_node))
|
||||
actions.add_child(btn_inspect)
|
||||
|
||||
info.add_child(actions)
|
||||
graph_node.set_slot(0, true, 0, Color(0.75, 0.75, 0.9), true, 0, Color(0.75, 0.9, 0.75))
|
||||
|
||||
if layout.has(node_id):
|
||||
graph_node.position_offset = layout[node_id]
|
||||
else:
|
||||
graph_node.position_offset = Vector2(60 + (auto_index % 5) * 320, 36 + int(auto_index / 5) * 160)
|
||||
auto_index += 1
|
||||
|
||||
_graph_edit.add_child(graph_node)
|
||||
node_name_by_id[node_id] = graph_node.name
|
||||
|
||||
for edge_data in edges:
|
||||
var from_id: String = edge_data.get("from", "")
|
||||
var to_id: String = edge_data.get("to", "")
|
||||
if not (node_name_by_id.has(from_id) and node_name_by_id.has(to_id)):
|
||||
continue
|
||||
var from_name: StringName = StringName(node_name_by_id[from_id])
|
||||
var to_name: StringName = StringName(node_name_by_id[to_id])
|
||||
if from_name == to_name:
|
||||
continue
|
||||
if _graph_edit.is_node_connected(from_name, 0, to_name, 0):
|
||||
continue
|
||||
_graph_edit.connect_node(from_name, 0, to_name, 0)
|
||||
|
||||
func _load_layout_metadata() -> Dictionary:
|
||||
if _current_resource == null:
|
||||
return {}
|
||||
if not _current_resource.has_meta(META_LAYOUT_KEY):
|
||||
return {}
|
||||
var data = _current_resource.get_meta(META_LAYOUT_KEY, {})
|
||||
return data if data is Dictionary else {}
|
||||
|
||||
func _save_layout_metadata() -> void:
|
||||
if _current_resource == null:
|
||||
return
|
||||
|
||||
var layout := {}
|
||||
for child in _graph_edit.get_children():
|
||||
if not (child is GraphNode):
|
||||
continue
|
||||
var graph_id: String = child.get_meta("graph_id", "")
|
||||
if graph_id == "":
|
||||
continue
|
||||
layout[graph_id] = child.position_offset
|
||||
|
||||
_current_resource.set_meta(META_LAYOUT_KEY, layout)
|
||||
if _current_resource.resource_path != "":
|
||||
var err := ResourceSaver.save(_current_resource, _current_resource.resource_path)
|
||||
if err != OK:
|
||||
_set_status("Failed to save graph layout metadata.", true)
|
||||
return
|
||||
|
||||
_set_status("Saved graph layout metadata.", false)
|
||||
|
||||
func _node_name_from_id(node_id: String) -> String:
|
||||
return "n_%s" % str(abs(node_id.hash()))
|
||||
|
||||
func _display_path(resource: Resource) -> String:
|
||||
if resource == null:
|
||||
return ""
|
||||
if resource.resource_path != "":
|
||||
return resource.resource_path
|
||||
return "<subresource>"
|
||||
|
||||
func _set_status(message: String, is_error: bool) -> void:
|
||||
if _status_label == null:
|
||||
return
|
||||
_status_label.text = message
|
||||
_status_label.modulate = Color(1, 0.55, 0.55) if is_error else Color(1, 1, 1)
|
||||
|
||||
func _on_graph_node_selected(node_arg) -> void:
|
||||
# GraphEdit may send different types (StringName, NodePath, or the node object itself).
|
||||
var graph_node: GraphNode = null
|
||||
if typeof(node_arg) == TYPE_OBJECT and node_arg is GraphNode:
|
||||
graph_node = node_arg
|
||||
else:
|
||||
# Try to resolve by path or name
|
||||
var resolved := _graph_edit.get_node_or_null(node_arg)
|
||||
if resolved is GraphNode:
|
||||
graph_node = resolved
|
||||
|
||||
if not (graph_node is GraphNode):
|
||||
_selected_node_resource = null
|
||||
_open_selected_button.disabled = true
|
||||
_inspect_selected_button.disabled = true
|
||||
_wait_checkbox.visible = false
|
||||
return
|
||||
|
||||
_selected_node_resource = _get_graph_node_resource(graph_node)
|
||||
var enabled := _selected_node_resource != null
|
||||
_open_selected_button.disabled = not enabled
|
||||
_inspect_selected_button.disabled = not enabled
|
||||
|
||||
if enabled and _resource_has_property(_selected_node_resource, "WaitForCompletion"):
|
||||
_wait_checkbox.visible = true
|
||||
_wait_checkbox.set_pressed_no_signal(bool(_selected_node_resource.get("WaitForCompletion")))
|
||||
else:
|
||||
_wait_checkbox.visible = false
|
||||
|
||||
_set_status("Selected: %s" % _display_path(_selected_node_resource), false)
|
||||
|
||||
func _on_node_open_pressed(graph_node: GraphNode) -> void:
|
||||
if not (graph_node is GraphNode):
|
||||
return
|
||||
var res := _get_graph_node_resource(graph_node)
|
||||
if res == null:
|
||||
_set_status("Node has no resource to open.", true)
|
||||
return
|
||||
open_resource(res)
|
||||
|
||||
func _on_toggle_details_pressed(graph_node: GraphNode, details: VBoxContainer) -> void:
|
||||
if details == null or graph_node == null:
|
||||
return
|
||||
details.visible = not details.visible
|
||||
if details.visible:
|
||||
var extra := details.get_combined_minimum_size().y
|
||||
graph_node.custom_minimum_size = Vector2(240, 84 + extra)
|
||||
else:
|
||||
graph_node.custom_minimum_size = Vector2(240, 84)
|
||||
|
||||
func _on_prop_text_changed(resource: Resource, prop_name: String, new_text: String) -> void:
|
||||
if resource == null:
|
||||
return
|
||||
resource.set(prop_name, new_text)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Updated %s" % prop_name, false)
|
||||
|
||||
func _on_prop_float_changed(resource: Resource, prop_name: String, value: float) -> void:
|
||||
if resource == null:
|
||||
return
|
||||
resource.set(prop_name, value)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Updated %s" % prop_name, false)
|
||||
|
||||
func _on_prop_bool_changed(resource: Resource, prop_name: String, pressed: bool) -> void:
|
||||
if resource == null:
|
||||
return
|
||||
resource.set(prop_name, pressed)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Updated %s" % prop_name, false)
|
||||
|
||||
func _on_add_shooting_pattern_pressed(resource: Resource) -> void:
|
||||
if resource == null:
|
||||
_set_status("No resource to add pattern to.", true)
|
||||
return
|
||||
# Try to instantiate a ShootingPattern3D (GlobalClass C# type)
|
||||
var new_pattern = null
|
||||
# attempt to instantiate ShootingPattern3D; fall back to generic Resource if not available
|
||||
# Instantiating a C# AttackPattern from GDScript can be fragile; create a generic Resource for now.
|
||||
new_pattern = Resource.new()
|
||||
# Append to Patterns array if present
|
||||
if _resource_has_property(resource, "Patterns"):
|
||||
var arr := resource.get("Patterns")
|
||||
if not (arr is Array):
|
||||
arr = []
|
||||
arr.append(new_pattern)
|
||||
resource.set("Patterns", arr)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Added new pattern.", false)
|
||||
_render_graph()
|
||||
else:
|
||||
_set_status("Resource has no Patterns array.", true)
|
||||
|
||||
func _on_node_inspect_pressed(graph_node: GraphNode) -> void:
|
||||
if not (graph_node is GraphNode):
|
||||
return
|
||||
var res := _get_graph_node_resource(graph_node)
|
||||
if res == null:
|
||||
_set_status("Node has no resource to inspect.", true)
|
||||
return
|
||||
if _editor_interface == null:
|
||||
_set_status("Editor interface not available.", true)
|
||||
return
|
||||
# Defer opening the inspector to avoid any re-entrancy or script-parse conflicts
|
||||
call_deferred("_deferred_edit_resource", res)
|
||||
|
||||
# Immediate status update; the actual inspector open happens shortly after
|
||||
_set_status("Inspecting (deferred): %s" % _display_path(res), false)
|
||||
|
||||
func _deferred_edit_resource(res: Resource) -> void:
|
||||
if _editor_interface == null or res == null:
|
||||
return
|
||||
_editor_interface.edit_resource(res)
|
||||
_set_status("Inspecting: %s" % _display_path(res), false)
|
||||
|
||||
func _on_open_selected_node_pressed() -> void:
|
||||
if _selected_node_resource == null:
|
||||
_set_status("No node resource selected.", true)
|
||||
return
|
||||
open_resource(_selected_node_resource)
|
||||
|
||||
func _on_inspect_selected_node_pressed() -> void:
|
||||
if _selected_node_resource == null:
|
||||
_set_status("No node resource selected.", true)
|
||||
return
|
||||
if _editor_interface == null:
|
||||
_set_status("Editor interface is not available.", true)
|
||||
return
|
||||
_editor_interface.edit_resource(_selected_node_resource)
|
||||
_set_status("Inspected selected node resource.", false)
|
||||
|
||||
func _unique_strings(values: Array) -> PackedStringArray:
|
||||
var seen := {}
|
||||
var result := PackedStringArray()
|
||||
for value in values:
|
||||
var text := str(value)
|
||||
if text == "" or seen.has(text):
|
||||
continue
|
||||
seen[text] = true
|
||||
result.append(text)
|
||||
if result.is_empty():
|
||||
result.append("-")
|
||||
return result
|
||||
|
||||
func _on_wait_for_completion_toggled(pressed: bool) -> void:
|
||||
if _selected_node_resource == null:
|
||||
return
|
||||
if not _resource_has_property(_selected_node_resource, "WaitForCompletion"):
|
||||
return
|
||||
|
||||
_selected_node_resource.set("WaitForCompletion", pressed)
|
||||
if _selected_node_resource.resource_path != "":
|
||||
var err := ResourceSaver.save(_selected_node_resource, _selected_node_resource.resource_path)
|
||||
if err != OK:
|
||||
_set_status("Failed to save WaitForCompletion.", true)
|
||||
return
|
||||
|
||||
_set_status("Updated WaitForCompletion on selected pattern.", false)
|
||||
_render_graph()
|
||||
|
||||
func _resource_has_property(resource: Object, property_name: String) -> bool:
|
||||
for property_data in resource.get_property_list():
|
||||
if property_data is Dictionary and property_data.get("name", "") == property_name:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _get_graph_node_resource(graph_node: GraphNode) -> Resource:
|
||||
if graph_node == null:
|
||||
return null
|
||||
if not graph_node.has_meta("resource"):
|
||||
return null
|
||||
var value = graph_node.get_meta("resource")
|
||||
return value if value is Resource else null
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://duc4xqfaiyjq5
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/bullet_script_graph_editor/ui/graph_editor_dock.gd" id="1_5n6ty"]
|
||||
|
||||
[node name="GraphEditorDock" type="PanelContainer"]
|
||||
script = ExtResource("1_5n6ty")
|
||||
65
docs/bullet-script-graph-editor-plan.md
Normal file
65
docs/bullet-script-graph-editor-plan.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Bullet Script Graph Editor Plan
|
||||
|
||||
## Goals
|
||||
- Create an editor plugin in GDScript using `GraphEdit` and `GraphNode` for visual authoring of bullet scripts.
|
||||
- Support opening from `BossScript`, `BossPhase`, `BulletScript3D`, or `BulletScript`.
|
||||
- Visualize parent/child pattern relationships as linked graph nodes.
|
||||
- Keep legacy `BossPhase.Patterns` intact to avoid breaking existing compositions.
|
||||
- Introduce `BossPhase -> BulletScript3D` for flexible composition.
|
||||
- Persist graph layout metadata so graph positions remain stable across sessions.
|
||||
|
||||
## Existing System Summary
|
||||
- `BossScript` contains phases: `Scripts/Resources/ScriptableBullets/BossScript.cs`.
|
||||
- `BossPhase` currently executes legacy `Patterns` (`Array<AttackPattern>`): `Scripts/Resources/BossPhase.cs`.
|
||||
- `BulletScript3D` and `BulletScript` are standalone pattern containers:
|
||||
- `Scripts/Resources/BulletScripts/BulletScript3D.cs`
|
||||
- `Scripts/Resources/BulletScript.cs`
|
||||
- Pattern composition already exists with:
|
||||
- Sequential group: `Scripts/Resources/PatternGroup.cs`
|
||||
- Parallel group: `Scripts/Resources/ScriptableBullets/ParallelPatternGroup.cs`
|
||||
|
||||
## Non-Breaking Runtime Changes
|
||||
1. Add `[Export] public BulletScript3D BulletScript3D;` to `BossPhase`.
|
||||
2. Keep `Patterns` unchanged and mark as legacy with a TODO comment.
|
||||
3. In `BossPhase.Start` and `BossPhase.UpdatePhase`, prioritize `BulletScript3D` execution when assigned.
|
||||
4. Fall back to legacy `Patterns` path when `BulletScript3D` is not assigned.
|
||||
5. Add null/empty guards for robust editor/runtime behavior.
|
||||
|
||||
## Plugin Architecture
|
||||
- New addon: `addons/bullet_script_graph_editor/`
|
||||
- Main files:
|
||||
- `plugin.cfg`
|
||||
- `plugin.gd` (`EditorPlugin` entry)
|
||||
- `inspector_plugin.gd` (open-in-graph button in inspector)
|
||||
- `ui/graph_editor_dock.tscn`
|
||||
- `ui/graph_editor_dock.gd`
|
||||
- `model/graph_adapter.gd`
|
||||
|
||||
### Entry Points
|
||||
- Bottom dock panel for direct resource picking/opening.
|
||||
- Inspector button for fast open from selected resource.
|
||||
|
||||
### Graph Mapping Rules
|
||||
- `BossScript` -> phase chain nodes.
|
||||
- `BossPhase` ->
|
||||
- legacy pattern chain lane from `Patterns`
|
||||
- optional linked `BulletScript3D` lane.
|
||||
- `BulletScript3D` / `BulletScript` -> ordered pattern chain.
|
||||
- `PatternGroup` and `ParallelPatternGroup` -> composite nodes with child links.
|
||||
|
||||
### Persistence
|
||||
- Store node graph position metadata on resources using editor metadata keys.
|
||||
- Use stable node IDs to restore positions across reopen.
|
||||
|
||||
## Validation Checklist
|
||||
1. Enable plugin and confirm dock is visible.
|
||||
2. Open `Resources/BossPhases/Rumia_Boss_Script_3D.tres` and verify phase/pattern graph.
|
||||
3. Open `Resources/BulletScripts/Danmaku_Room_1.tres` and verify pattern chain graph.
|
||||
4. Save, reopen, and verify node positions persist.
|
||||
5. Run build:
|
||||
- `dotnet build k:/godot/cirno/Cirno.csproj -c Debug -v normal -p:GodotTargetPlatform=windows`
|
||||
|
||||
## Out-of-Scope for Initial Cut
|
||||
- Removing legacy `Patterns`.
|
||||
- Full simulation/debug playback inside the graph UI.
|
||||
- Automatic bulk migration of all existing resources.
|
||||
|
|
@ -197,7 +197,7 @@ movie_writer/movie_file="D:/Maddo/Recordings/Capture.avi"
|
|||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/cyclops_level_builder/plugin.cfg", "res://addons/dialogic/plugin.cfg", "res://addons/func_godot/plugin.cfg", "res://addons/gdai-mcp-plugin-godot/plugin.cfg", "res://addons/godot_test_scene/plugin.cfg", "res://addons/resources_spreadsheet_view/plugin.cfg", "res://addons/scene_palette/plugin.cfg", "res://addons/smoothing/plugin.cfg", "res://addons/tattomoosa.vision_cone_3d/plugin.cfg", "res://addons/weapon_creator/plugin.cfg")
|
||||
enabled=PackedStringArray("res://addons/bullet_script_graph_editor/plugin.cfg", "res://addons/cyclops_level_builder/plugin.cfg", "res://addons/dialogic/plugin.cfg", "res://addons/func_godot/plugin.cfg", "res://addons/gdai-mcp-plugin-godot/plugin.cfg", "res://addons/godot_test_scene/plugin.cfg", "res://addons/resources_spreadsheet_view/plugin.cfg", "res://addons/scene_palette/plugin.cfg", "res://addons/smoothing/plugin.cfg", "res://addons/tattomoosa.vision_cone_3d/plugin.cfg", "res://addons/weapon_creator/plugin.cfg")
|
||||
|
||||
[func_godot]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue