Files
2026-07-06 22:59:47 -07:00

198 lines
6.3 KiB
GDScript

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)