generated from krampus/template-godot4
177 lines
4.7 KiB
GDScript3
177 lines
4.7 KiB
GDScript3
|
@tool
|
||
|
class_name RandomizedComposite extends Composite
|
||
|
|
||
|
const WEIGHTS_PREFIX = "Weights/"
|
||
|
|
||
|
## Sets a predicable seed
|
||
|
@export var random_seed: int = 0:
|
||
|
set(rs):
|
||
|
random_seed = rs
|
||
|
if random_seed != 0:
|
||
|
seed(random_seed)
|
||
|
else:
|
||
|
randomize()
|
||
|
|
||
|
## Wether to use weights for every child or not.
|
||
|
@export var use_weights: bool:
|
||
|
set(value):
|
||
|
use_weights = value
|
||
|
if use_weights:
|
||
|
_update_weights(get_children())
|
||
|
_connect_children_changing_signals()
|
||
|
notify_property_list_changed()
|
||
|
|
||
|
var _weights: Dictionary
|
||
|
var _exiting_tree: bool
|
||
|
|
||
|
|
||
|
func _ready():
|
||
|
_connect_children_changing_signals()
|
||
|
|
||
|
|
||
|
func _connect_children_changing_signals():
|
||
|
if not child_entered_tree.is_connected(_on_child_entered_tree):
|
||
|
child_entered_tree.connect(_on_child_entered_tree)
|
||
|
|
||
|
if not child_exiting_tree.is_connected(_on_child_exiting_tree):
|
||
|
child_exiting_tree.connect(_on_child_exiting_tree)
|
||
|
|
||
|
|
||
|
func get_shuffled_children() -> Array[Node]:
|
||
|
var children_bag: Array[Node] = get_children().duplicate()
|
||
|
if use_weights:
|
||
|
var weights: Array[int]
|
||
|
weights.assign(children_bag.map(func(child): return _weights[child.name]))
|
||
|
children_bag.assign(_weighted_shuffle(children_bag, weights))
|
||
|
else:
|
||
|
children_bag.shuffle()
|
||
|
return children_bag
|
||
|
|
||
|
|
||
|
## Returns a shuffled version of a given array using the supplied array of weights.
|
||
|
## Think of weights as the chance of a given item being the first in the array.
|
||
|
func _weighted_shuffle(items: Array, weights: Array[int]) -> Array:
|
||
|
if len(items) != len(weights):
|
||
|
push_error(
|
||
|
(
|
||
|
"items and weights size mismatch: expected %d weights, got %d instead."
|
||
|
% [len(items), len(weights)]
|
||
|
)
|
||
|
)
|
||
|
return items
|
||
|
|
||
|
# This method is based on the weighted random sampling algorithm
|
||
|
# by Efraimidis, Spirakis; 2005. This runs in O(n log(n)).
|
||
|
|
||
|
# For each index, it will calculate random_value^(1/weight).
|
||
|
var chance_calc = func(i): return [i, randf() ** (1.0 / weights[i])]
|
||
|
var random_distribuition = range(len(items)).map(chance_calc)
|
||
|
|
||
|
# Now we just have to order by the calculated value, descending.
|
||
|
random_distribuition.sort_custom(func(a, b): return a[1] > b[1])
|
||
|
|
||
|
return random_distribuition.map(func(dist): return items[dist[0]])
|
||
|
|
||
|
|
||
|
func _get_property_list():
|
||
|
var properties = []
|
||
|
|
||
|
if use_weights:
|
||
|
for key in _weights.keys():
|
||
|
properties.append(
|
||
|
{
|
||
|
"name": WEIGHTS_PREFIX + key,
|
||
|
"type": TYPE_INT,
|
||
|
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
|
||
|
"hint": PROPERTY_HINT_RANGE,
|
||
|
"hint_string": "1,100"
|
||
|
}
|
||
|
)
|
||
|
|
||
|
return properties
|
||
|
|
||
|
|
||
|
func _set(property: StringName, value: Variant) -> bool:
|
||
|
if property.begins_with(WEIGHTS_PREFIX):
|
||
|
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
|
||
|
_weights[weight_name] = value
|
||
|
return true
|
||
|
|
||
|
return false
|
||
|
|
||
|
|
||
|
func _get(property: StringName):
|
||
|
if property.begins_with(WEIGHTS_PREFIX):
|
||
|
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
|
||
|
return _weights[weight_name]
|
||
|
|
||
|
return null
|
||
|
|
||
|
|
||
|
func _update_weights(children: Array[Node]) -> void:
|
||
|
var new_weights = {}
|
||
|
for c in children:
|
||
|
if _weights.has(c.name):
|
||
|
new_weights[c.name] = _weights[c.name]
|
||
|
else:
|
||
|
new_weights[c.name] = 1
|
||
|
_weights = new_weights
|
||
|
notify_property_list_changed()
|
||
|
|
||
|
|
||
|
func _exit_tree() -> void:
|
||
|
_exiting_tree = true
|
||
|
|
||
|
|
||
|
func _enter_tree() -> void:
|
||
|
_exiting_tree = false
|
||
|
|
||
|
|
||
|
func _on_child_entered_tree(node: Node):
|
||
|
_update_weights(get_children())
|
||
|
|
||
|
var renamed_callable = _on_child_renamed.bind(node.name, node)
|
||
|
if not node.renamed.is_connected(renamed_callable):
|
||
|
node.renamed.connect(renamed_callable)
|
||
|
|
||
|
if not node.tree_exited.is_connected(_on_child_tree_exited):
|
||
|
node.tree_exited.connect(_on_child_tree_exited.bind(node))
|
||
|
|
||
|
|
||
|
func _on_child_exiting_tree(node: Node):
|
||
|
var renamed_callable = _on_child_renamed.bind(node.name, node)
|
||
|
if node.renamed.is_connected(renamed_callable):
|
||
|
node.renamed.disconnect(renamed_callable)
|
||
|
|
||
|
|
||
|
func _on_child_tree_exited(node: Node) -> void:
|
||
|
# don't erase the individual child if the whole tree is exiting together
|
||
|
if not _exiting_tree:
|
||
|
var children = get_children()
|
||
|
children.erase(node)
|
||
|
_update_weights(children)
|
||
|
|
||
|
if node.tree_exited.is_connected(_on_child_tree_exited):
|
||
|
node.tree_exited.disconnect(_on_child_tree_exited)
|
||
|
|
||
|
|
||
|
func _on_child_renamed(old_name: String, renamed_child: Node):
|
||
|
if old_name == renamed_child.name:
|
||
|
return # No need to update the weights.
|
||
|
|
||
|
# Disconnect signal with old name...
|
||
|
renamed_child.renamed.disconnect(_on_child_renamed.bind(old_name, renamed_child))
|
||
|
# ...and connect with the new name.
|
||
|
renamed_child.renamed.connect(_on_child_renamed.bind(renamed_child.name, renamed_child))
|
||
|
|
||
|
var original_weight = _weights[old_name]
|
||
|
_weights.erase(old_name)
|
||
|
_weights[renamed_child.name] = original_weight
|
||
|
notify_property_list_changed()
|
||
|
|
||
|
|
||
|
func get_class_name() -> Array[StringName]:
|
||
|
var classes := super()
|
||
|
classes.push_back(&"RandomizedComposite")
|
||
|
return classes
|