class_name ActionResolver extends Node const ACTION_DIR := "res://resources/actions" static var _loaded := false static var _actions_by_pattern: Dictionary = {} static var _actions_by_id: Dictionary = {} static var _space_priority_labels: Array[StringName] = [ &"charge_release", &"burst", &"counter_projectile", &"blade_chain", &"state_specific", &"exact_pattern", &"fallback", ] func resolve_window(window: Variant, state_machine: Variant = null, context: Dictionary = {}) -> Resource: return ActionResolver.resolve(window, state_machine, context) func resolve_text_pattern(pattern: String, state_machine: Variant = null, context: Dictionary = {}) -> Resource: return ActionResolver.resolve_pattern(pattern, state_machine, context) static func resolve(window: Variant, state_machine: Variant = null, context: Dictionary = {}) -> Resource: return resolve_pattern(window.get_contiguous_pattern(), state_machine, context) static func resolve_pattern(pattern: String, state_machine: Variant = null, context: Dictionary = {}) -> Resource: _ensure_loaded() var key := _normalize_pattern(pattern) if key.ends_with("SP"): var priority_result := _resolve_space_priority(key, state_machine, context) if priority_result != null: return priority_result var candidates: Array = _actions_by_pattern.get(key, []) var state := _state_name(state_machine, context) for action: Resource in candidates: if _can_use_in_state(action, state): return action return null static func get_action(action_id: StringName) -> Resource: _ensure_loaded() return _actions_by_id.get(action_id, null) as Resource static func space_priority_labels() -> Array[StringName]: return _space_priority_labels.duplicate() static func reload(action_dir := ACTION_DIR) -> void: _loaded = true _actions_by_pattern.clear() _actions_by_id.clear() _load_dir(action_dir) static func clear_cache() -> void: _actions_by_pattern.clear() _actions_by_id.clear() _loaded = false static func _ensure_loaded() -> void: if not _loaded: reload() static func _load_dir(action_dir: String) -> void: var dir := DirAccess.open(action_dir) if dir == null: return var files := dir.get_files() files.sort() for file_name: String in files: if not file_name.ends_with(".tres"): continue var action: Resource = load("%s/%s" % [action_dir, file_name]) if action == null or not action.get("input_pattern") is Array: continue _register_action(action) static func _register_action(action: Resource) -> void: var key := _pattern_key(action.get("input_pattern")) if key.is_empty(): return var candidates: Array = _actions_by_pattern.get(key, []) candidates.append(action) _actions_by_pattern[key] = candidates var id := StringName(str(action.get("id"))) if not id.is_empty(): _actions_by_id[id] = action static func _resolve_space_priority(key: String, state_machine: Variant, context: Dictionary) -> Resource: var state := _state_name(state_machine, context) for action_id_key: String in [ "charge_release_action_id", "burst_action_id", "counter_action_id", "blade_chain_action_id", ]: if not context.has(action_id_key): continue if action_id_key == "counter_action_id" and not bool(context.get("counter_ready", false)): continue if action_id_key == "blade_chain_action_id" and not bool(context.get("blade_chain_active", false)): continue var explicit_action := get_action(StringName(str(context[action_id_key]))) if explicit_action != null and _can_use_in_state(explicit_action, state): return explicit_action var candidates: Array = _actions_by_pattern.get(key, []) for action: Resource in candidates: if _can_use_in_state(action, state): return action var suffix_action := _resolve_trailing_space_suffix(key, state) if suffix_action != null: return suffix_action return null static func _resolve_trailing_space_suffix(key: String, state: StringName) -> Resource: var best_action: Resource = null var best_length := 0 for candidate_key: String in _actions_by_pattern.keys(): if candidate_key == key: continue if not candidate_key.ends_with("SP"): continue if candidate_key.length() <= best_length: continue if not key.ends_with(candidate_key): continue for action: Resource in _actions_by_pattern.get(candidate_key, []): if _can_use_in_state(action, state): best_action = action best_length = candidate_key.length() break return best_action static func _pattern_key(pattern: Array[StringName]) -> String: var key := "" for symbol: StringName in pattern: key += str(symbol) return _normalize_pattern(key) static func _normalize_pattern(pattern: String) -> String: return pattern.replace(" ", "").to_upper() static func _state_name(state_machine: Variant, context: Dictionary) -> StringName: if context.has("state"): return StringName(str(context["state"])) if state_machine != null and state_machine.has_method("get_current_state_name"): return StringName(str(state_machine.call("get_current_state_name"))) return &"any" static func _can_use_in_state(action: Resource, state: StringName) -> bool: var required := StringName(str(action.get("required_state"))) return required.is_empty() or required == &"any" or state == &"any" or required == state