class_name ActionResolver extends Node const ACTION_DIR := "res://resources/actions" const ActionRuleResolverScript := preload("res://scripts/resolvers/action_rule_resolver.gd") const BASIC_TRAILING_SUFFIXES := ["W", "A", "D", "S"] static var _loaded := false static var _actions_by_pattern: Dictionary = {} static var _actions_by_id: Dictionary = {} static var _space_priority_labels: Array[StringName] = [ &"burst", &"counter_projectile", &"blade_chain", &"state_specific", &"exact_pattern", &"fallback", ] func resolve_window(window: Variant, state_machine: Variant = null, context: Dictionary = {}) -> Resource: return resolve(window, state_machine, context) func resolve_text_pattern(pattern: String, state_machine: Variant = null, context: Dictionary = {}) -> Resource: return 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 = {}, allow_basic_trailing_fallback := true) -> Resource: _ensure_loaded() var key := _normalize_pattern(pattern) var resolver_context := _resolver_context(state_machine, context) if key.ends_with("SP"): var priority_result := _resolve_space_priority(key, resolver_context) if priority_result != null: return priority_result var candidates: Array = _actions_by_pattern.get(key, []) for action: Resource in candidates: if _can_execute(action, resolver_context): return action var trailing_basic := _resolve_trailing_basic_suffix(key, resolver_context, allow_basic_trailing_fallback) if trailing_basic != null: return trailing_basic 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 subdirs := dir.get_directories() subdirs.sort() for subdir: String in subdirs: _load_dir("%s/%s" % [action_dir, subdir]) # 导出包(pck)内的目录列表把资源显示为 xxx.tres.remap,直接按 .tres # 过滤会一个都匹配不上,导致动作库为空(玩家出招和敌人攻击全部失效)。 # 还原原始文件名再 load(),remap 表会命中 pck 内的实际资源。 var seen := {} var files := dir.get_files() files.sort() for file_name: String in files: var resource_name := file_name.trim_suffix(".remap") if not resource_name.ends_with(".tres") or seen.has(resource_name): continue seen[resource_name] = true var action: Resource = load("%s/%s" % [action_dir, resource_name]) if action == null or not action.get("input_pattern") is Array: continue _register_action(action) static func _register_action(action: Resource) -> void: var id := StringName(str(action.get("id"))) if not id.is_empty(): _actions_by_id[id] = action 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 static func _resolve_space_priority(key: String, context: Dictionary) -> Resource: for action_id_key: String in [ "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_execute(explicit_action, context): return explicit_action var candidates: Array = _actions_by_pattern.get(key, []) for action: Resource in candidates: if _can_execute(action, context): return action var suffix_action := _resolve_trailing_space_suffix(key, context) if suffix_action != null: return suffix_action return null static func _resolve_trailing_space_suffix(key: String, context: Dictionary) -> 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_execute(action, context): best_action = action best_length = candidate_key.length() break return best_action static func _resolve_trailing_basic_suffix(key: String, context: Dictionary, allow_basic_trailing_fallback: bool) -> Resource: if not allow_basic_trailing_fallback: return null for candidate_key: String in BASIC_TRAILING_SUFFIXES: if key == candidate_key: continue if not key.ends_with(candidate_key): continue for action: Resource in _actions_by_pattern.get(candidate_key, []): if _can_execute(action, context): return action return null 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 _resolver_context(state_machine: Variant, context: Dictionary) -> Dictionary: var result := context.duplicate() if state_machine != null and state_machine.has_method("build_context"): for key: Variant in state_machine.call("build_context").keys(): if not result.has(key): result[key] = state_machine.call("build_context")[key] return result static func _can_execute(action: Resource, context: Dictionary) -> bool: return ActionRuleResolverScript.can_execute(context, action)