Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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
|
||||
Reference in New Issue
Block a user