Files
TheTree_mock/scripts/events/event_condition.gd
T

215 lines
6.2 KiB
GDScript

class_name EventCondition
extends RefCounted
## Side-effect-free, data-only event predicate.
enum Operator {
EQ,
NE,
LT,
LTE,
GT,
GTE,
HAS_FLAG,
NOT_HAS_FLAG,
IN_RANGE,
ALL,
ANY,
NOT,
ENTITY_EXISTS,
PATH_EXISTS,
TIME_IN_WINDOW,
}
var operator: Operator = Operator.EQ
var path: Array[StringName] = []
var expected: Variant = null
var children: Array[EventCondition] = []
func configure(
p_operator: Variant,
p_path: Variant = [],
p_expected: Variant = null,
p_children: Array = []
) -> EventCondition:
operator = _operator_from_variant(p_operator)
path = _normalize_path(p_path)
expected = _copy(p_expected)
children.clear()
for raw_child: Variant in p_children:
var child := from_variant(raw_child)
if child != null:
children.append(child)
return self
func is_valid() -> bool:
if operator < Operator.EQ or operator > Operator.TIME_IN_WINDOW:
return false
if operator in [Operator.ALL, Operator.ANY] and children.is_empty():
return false
if operator == Operator.NOT and children.size() != 1:
return false
if operator not in [Operator.ALL, Operator.ANY, Operator.NOT, Operator.TIME_IN_WINDOW] and path.is_empty():
return false
for child: EventCondition in children:
if child == null or not child.is_valid():
return false
return true
func evaluate(context: Dictionary, callbacks: Dictionary = {}) -> bool:
match operator:
Operator.ALL:
for child: EventCondition in children:
if not child.evaluate(context, callbacks):
return false
return true
Operator.ANY:
for child: EventCondition in children:
if child.evaluate(context, callbacks):
return true
return false
Operator.NOT:
return not children[0].evaluate(context, callbacks)
Operator.TIME_IN_WINDOW:
var minute := int(_resolve(context, path)) if not path.is_empty() else int(_dictionary_value(context, &"absolute_minute", 0))
return _time_in_window(minute, expected)
var actual: Variant = _resolve(context, path)
match operator:
Operator.EQ:
return actual == expected
Operator.NE:
return actual != expected
Operator.LT:
return _numeric(actual) < _numeric(expected)
Operator.LTE:
return _numeric(actual) <= _numeric(expected)
Operator.GT:
return _numeric(actual) > _numeric(expected)
Operator.GTE:
return _numeric(actual) >= _numeric(expected)
Operator.HAS_FLAG:
return _has_flag(actual, expected)
Operator.NOT_HAS_FLAG:
return not _has_flag(actual, expected)
Operator.IN_RANGE:
return _in_range(actual, expected)
Operator.ENTITY_EXISTS:
return _callback_predicate(callbacks, &"entity_exists", actual)
Operator.PATH_EXISTS:
return _callback_predicate(callbacks, &"path_exists", actual)
return false
func create_snapshot() -> Dictionary:
var child_snapshots: Array[Dictionary] = []
for child: EventCondition in children:
child_snapshots.append(child.create_snapshot())
var path_strings: Array[String] = []
for part: StringName in path:
path_strings.append(String(part))
return {
&"operator": Operator.keys()[operator],
&"path": path_strings,
&"expected": _copy(expected),
&"children": child_snapshots,
}
static func from_variant(raw_value: Variant) -> EventCondition:
if raw_value is EventCondition:
return raw_value
if not raw_value is Dictionary:
return null
var raw_children: Variant = _dictionary_value(raw_value, &"children", [])
return EventCondition.new().configure(
_dictionary_value(raw_value, &"operator", "EQ"),
_dictionary_value(raw_value, &"path", []),
_dictionary_value(raw_value, &"expected", _dictionary_value(raw_value, &"value", null)),
raw_children if raw_children is Array else []
)
static func _operator_from_variant(raw_value: Variant) -> Operator:
if raw_value is int:
return clampi(int(raw_value), Operator.EQ, Operator.TIME_IN_WINDOW) as Operator
var index := Operator.keys().find(String(raw_value).to_upper())
return (index if index >= 0 else Operator.EQ) as Operator
static func _normalize_path(raw_value: Variant) -> Array[StringName]:
var result: Array[StringName] = []
if raw_value is String or raw_value is StringName:
for part: String in String(raw_value).split(".", false):
result.append(StringName(part))
elif raw_value is Array:
for raw_part: Variant in raw_value:
var part := StringName(raw_part)
if not part.is_empty():
result.append(part)
return result
static func _resolve(context: Dictionary, field_path: Array[StringName]) -> Variant:
var current: Variant = context
for part: StringName in field_path:
if not current is Dictionary:
return null
current = _dictionary_value(current, part, null)
return current
static func _numeric(value: Variant) -> float:
return float(value) if value is int or value is float else 0.0
static func _has_flag(actual: Variant, flag_value: Variant) -> bool:
var flag := StringName(flag_value)
if actual is Dictionary:
return bool(_dictionary_value(actual, flag, false))
if actual is Array:
for raw_entry: Variant in actual:
if StringName(raw_entry) == flag:
return true
return false
static func _in_range(actual: Variant, bounds: Variant) -> bool:
if not bounds is Array or bounds.size() < 2:
return false
var numeric := _numeric(actual)
return numeric >= _numeric(bounds[0]) and numeric <= _numeric(bounds[1])
static func _time_in_window(absolute_minute: int, raw_window: Variant) -> bool:
if not raw_window is Array or raw_window.size() < 2:
return false
var minute_of_day := posmod(absolute_minute, 1_440)
var start := posmod(int(raw_window[0]), 1_440)
var finish := posmod(int(raw_window[1]), 1_440)
if start == finish:
return true
if start < finish:
return minute_of_day >= start and minute_of_day < finish
return minute_of_day >= start or minute_of_day < finish
static func _callback_predicate(callbacks: Dictionary, callback_id: StringName, value: Variant) -> bool:
var callback: Variant = _dictionary_value(callbacks, callback_id, Callable())
if callback is Callable and callback.is_valid():
return bool(callback.call(value))
return false
static func _dictionary_value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
if source.has(key):
return source[key]
return source.get(String(key), fallback)
static func _copy(value: Variant) -> Variant:
return value.duplicate(true) if value is Dictionary or value is Array else value