Initial project sync
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
class_name ActionRuleResolver
|
||||
extends RefCounted
|
||||
|
||||
|
||||
static func can_execute(context: Dictionary, action: Resource) -> bool:
|
||||
if action == null:
|
||||
return false
|
||||
var life_state := _context_axis(context, "life_state", &"Alive")
|
||||
if life_state != &"Alive":
|
||||
return false
|
||||
var allowed_ground := _string_name_array(action.get("allowed_ground_states"))
|
||||
if not allowed_ground.is_empty() and not allowed_ground.has(_context_axis(context, "ground_state", &"Grounded")):
|
||||
return false
|
||||
var allowed_phases := _string_name_array(action.get("allowed_action_phases"))
|
||||
if not allowed_phases.is_empty() and not allowed_phases.has(_context_axis(context, "action_phase", &"Neutral")):
|
||||
return false
|
||||
if not _passes_effect_rule_modifiers(context, action):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func can_move_freely(context: Dictionary) -> bool:
|
||||
if _context_axis(context, "life_state", &"Alive") != &"Alive":
|
||||
return false
|
||||
if _context_axis(context, "action_phase", &"Neutral") != &"Neutral":
|
||||
return false
|
||||
for modifier: Resource in _action_rule_modifiers(context):
|
||||
if bool(modifier.get("block_movement")):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _context_axis(context: Dictionary, key: String, fallback: StringName) -> StringName:
|
||||
if context.has(key):
|
||||
return StringName(str(context[key]))
|
||||
return fallback
|
||||
|
||||
|
||||
static func _string_name_array(value: Variant) -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
if value is Array:
|
||||
for item: Variant in value:
|
||||
result.append(StringName(str(item)))
|
||||
return result
|
||||
|
||||
|
||||
static func _passes_effect_rule_modifiers(context: Dictionary, action: Resource) -> bool:
|
||||
var action_tags := _string_name_array(action.get("action_tags"))
|
||||
for modifier: Resource in _action_rule_modifiers(context):
|
||||
var blocked_tags := _string_name_array(modifier.get("blocked_action_tags"))
|
||||
if not blocked_tags.is_empty() and _has_any(action_tags, blocked_tags):
|
||||
return false
|
||||
var allowed_tags := _string_name_array(modifier.get("allowed_action_tags"))
|
||||
if not allowed_tags.is_empty() and not _has_any(action_tags, allowed_tags):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _action_rule_modifiers(context: Dictionary) -> Array[Resource]:
|
||||
if context.has("action_rule_modifiers"):
|
||||
return _resource_array(context["action_rule_modifiers"])
|
||||
var container = context.get("effect_container", null)
|
||||
if container != null and container.has_method("action_rule_modifiers"):
|
||||
return _resource_array(container.call("action_rule_modifiers"))
|
||||
return []
|
||||
|
||||
|
||||
static func _resource_array(value: Variant) -> Array[Resource]:
|
||||
var result: Array[Resource] = []
|
||||
if value is Array:
|
||||
for item: Variant in value:
|
||||
if item is Resource:
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
static func _has_any(values: Array[StringName], candidates: Array[StringName]) -> bool:
|
||||
for value: StringName in values:
|
||||
if candidates.has(value):
|
||||
return true
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://crbvbt3lx55cx
|
||||
@@ -0,0 +1,122 @@
|
||||
class_name CombatResolver
|
||||
extends RefCounted
|
||||
|
||||
const StatResolverScript := preload("res://scripts/resolvers/stat_resolver.gd")
|
||||
const DefenseResolverScript := preload("res://scripts/resolvers/defense_resolver.gd")
|
||||
|
||||
|
||||
static func resolve_hit(emitter: Area2D, receiver: Area2D) -> Dictionary:
|
||||
var action: Resource = emitter.get("action_context") as Resource
|
||||
var judgement: Dictionary = emitter.get("judgement_context")
|
||||
var receiver_context := _receiver_context(receiver, emitter)
|
||||
var attacker_effects := _owner_effect_container(emitter)
|
||||
var defense := DefenseResolverScript.resolve_effective_defense(receiver_context, _action_tags(action))
|
||||
var raw_damage := StatResolverScript.resolve_damage(float(emitter.get("damage")), action, judgement, attacker_effects, null)
|
||||
var final_damage := int(round(raw_damage * float(defense.get("damage_mult", 1.0))))
|
||||
var attacker_interrupts := _attacker_interrupts(emitter)
|
||||
# 受击方免压制钩子(2026-07-05 定案):Boss 对不耗能量的攻击恒霸体——
|
||||
# 伤害照常结算,但打断与击退一并取消。
|
||||
if attacker_interrupts and _receiver_shrugs_off(receiver, action):
|
||||
attacker_interrupts = false
|
||||
var knockback := _resolved_knockback(emitter, action, judgement, attacker_effects, defense) if attacker_interrupts else Vector2.ZERO
|
||||
return {
|
||||
"damage": final_damage,
|
||||
"hit_type": StringName(str(emitter.get("hit_type"))),
|
||||
"action": action,
|
||||
"action_tags": _action_tags(action),
|
||||
"judgement": judgement,
|
||||
"from": emitter.global_position,
|
||||
"emitter": emitter,
|
||||
"receiver": receiver,
|
||||
"defense": defense,
|
||||
"interrupts": bool(defense.get("interrupts", true)) and attacker_interrupts,
|
||||
"knockback": knockback,
|
||||
}
|
||||
|
||||
|
||||
static func _attacker_interrupts(emitter: Area2D) -> bool:
|
||||
var value = emitter.get("attacker_interrupts")
|
||||
if value is bool:
|
||||
return value
|
||||
return true
|
||||
|
||||
|
||||
static func _receiver_shrugs_off(receiver: Area2D, action: Resource) -> bool:
|
||||
var owner := receiver.get_parent()
|
||||
if owner == null or not owner.has_method("shrugs_off_hit"):
|
||||
return false
|
||||
return bool(owner.call("shrugs_off_hit", action))
|
||||
|
||||
|
||||
static func _receiver_context(receiver: Area2D, emitter: Area2D = null) -> Dictionary:
|
||||
var owner := receiver.get_parent()
|
||||
if owner != null:
|
||||
var context := {}
|
||||
var state_machine := owner.get_node_or_null("StateMachine")
|
||||
if state_machine != null and state_machine.has_method("build_context"):
|
||||
context = state_machine.call("build_context")
|
||||
var effect_container := owner.get_node_or_null("EffectContainer")
|
||||
if effect_container != null:
|
||||
context["effect_container"] = effect_container
|
||||
var block_judgement := _receiver_block_judgement(owner)
|
||||
if not block_judgement.is_empty():
|
||||
context["block_judgement"] = block_judgement
|
||||
if emitter != null and owner is Node2D:
|
||||
context["attack_from_front"] = _attack_from_front(owner as Node2D, emitter)
|
||||
return context
|
||||
return {}
|
||||
|
||||
|
||||
static func _receiver_block_judgement(owner: Node) -> StringName:
|
||||
var action_controller := owner.get_node_or_null("ActionController")
|
||||
if action_controller == null:
|
||||
return &""
|
||||
var intent = action_controller.get("current_intent")
|
||||
if intent == null:
|
||||
return &""
|
||||
var judgement = intent.get("judgement")
|
||||
if judgement is Dictionary and judgement.has("label"):
|
||||
return StringName(str(judgement["label"]))
|
||||
return &""
|
||||
|
||||
|
||||
static func _attack_from_front(owner: Node2D, emitter: Area2D) -> bool:
|
||||
var heading = owner.get("heading")
|
||||
if not (heading is Vector2) or is_zero_approx((heading as Vector2).x):
|
||||
return true
|
||||
var to_attacker := emitter.global_position.x - owner.global_position.x
|
||||
if is_zero_approx(to_attacker):
|
||||
return true
|
||||
return signf(to_attacker) == signf((heading as Vector2).x)
|
||||
|
||||
|
||||
static func _owner_effect_container(area: Area2D) -> Node:
|
||||
var source = area.get("source_actor")
|
||||
if source is Node and is_instance_valid(source):
|
||||
return (source as Node).get_node_or_null("EffectContainer")
|
||||
var owner := area.get_parent()
|
||||
if owner == null:
|
||||
return null
|
||||
return owner.get_node_or_null("EffectContainer")
|
||||
|
||||
|
||||
static func _action_tags(action: Resource) -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
if action != null and action.get("action_tags") is Array:
|
||||
for tag: Variant in action.get("action_tags"):
|
||||
result.append(StringName(str(tag)))
|
||||
return result
|
||||
|
||||
|
||||
static func _resolved_knockback(emitter: Area2D, action: Resource, judgement: Dictionary, attacker_effects: Variant, defense: Dictionary) -> Vector2:
|
||||
var defense_state := StringName(str(defense.get("defense_state", &"Vulnerable")))
|
||||
if defense_state == &"Invincible" or defense_state == &"SuperArmor":
|
||||
return Vector2.ZERO
|
||||
return StatResolverScript.resolve_knockback(action, judgement, _base_knockback(emitter), attacker_effects)
|
||||
|
||||
|
||||
static func _base_knockback(emitter: Area2D) -> Vector2:
|
||||
var value = emitter.get("base_knockback")
|
||||
if value is Vector2:
|
||||
return value
|
||||
return Vector2.ZERO
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbhmsc608i4f5
|
||||
@@ -0,0 +1,111 @@
|
||||
class_name DefenseResolver
|
||||
extends RefCounted
|
||||
|
||||
|
||||
static func resolve_effective_defense(context: Dictionary, attack_tags: Array[StringName] = []) -> Dictionary:
|
||||
var defense_state := StringName(str(context.get("defense_state", &"Vulnerable")))
|
||||
# 背刺破格挡只作用于姿态本身的 Parrying,先降级再合并 effect 修饰符:
|
||||
# 否则霸体(SuperArmor,优先级低于 Parrying)会先被格挡态吞掉、
|
||||
# 再随背刺降级一起丢失,违反霸体「无击退、不打断」的承诺。
|
||||
if defense_state == &"Parrying" and not bool(context.get("attack_from_front", true)):
|
||||
defense_state = &"Vulnerable"
|
||||
var modifiers := _defense_modifiers(context, attack_tags)
|
||||
for modifier: Resource in modifiers:
|
||||
var modified_state := StringName(str(modifier.get("defense_state")))
|
||||
if not modified_state.is_empty():
|
||||
defense_state = _stronger_defense_state(defense_state, modified_state)
|
||||
var damage_mult := _damage_mult_for_state(defense_state, context)
|
||||
var interrupts := defense_state != &"Invincible" and defense_state != &"SuperArmor" and defense_state != &"Parrying"
|
||||
for modifier: Resource in modifiers:
|
||||
damage_mult *= float(modifier.get("damage_mult"))
|
||||
interrupts = interrupts and bool(modifier.get("interrupts"))
|
||||
return {
|
||||
"defense_state": defense_state,
|
||||
"damage_mult": damage_mult,
|
||||
"interrupts": interrupts,
|
||||
}
|
||||
|
||||
|
||||
static func _damage_mult_for_state(defense_state: StringName, context: Dictionary = {}) -> float:
|
||||
match defense_state:
|
||||
&"Invincible":
|
||||
return 0.0
|
||||
&"Parrying":
|
||||
return _parry_damage_mult(context)
|
||||
&"SuperArmor":
|
||||
return 1.0
|
||||
_:
|
||||
return 1.0
|
||||
|
||||
|
||||
static func _parry_damage_mult(context: Dictionary) -> float:
|
||||
match StringName(str(context.get("block_judgement", &"perfect"))):
|
||||
&"good":
|
||||
return 0.2
|
||||
&"bad":
|
||||
return 0.4
|
||||
&"miss":
|
||||
return 1.0
|
||||
return 0.0
|
||||
|
||||
|
||||
static func _defense_modifiers(context: Dictionary, attack_tags: Array[StringName]) -> Array[Resource]:
|
||||
var modifiers: Array[Resource] = []
|
||||
if context.has("defense_modifiers"):
|
||||
modifiers = _resource_array(context["defense_modifiers"])
|
||||
else:
|
||||
var container = context.get("effect_container", null)
|
||||
if container != null and container.has_method("defense_modifiers"):
|
||||
modifiers = _resource_array(container.call("defense_modifiers"))
|
||||
var matched: Array[Resource] = []
|
||||
for modifier: Resource in modifiers:
|
||||
if _modifier_matches_attack_tags(modifier, attack_tags):
|
||||
matched.append(modifier)
|
||||
return matched
|
||||
|
||||
|
||||
static func _resource_array(value: Variant) -> Array[Resource]:
|
||||
var result: Array[Resource] = []
|
||||
if value is Array:
|
||||
for item: Variant in value:
|
||||
if item is Resource:
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
static func _modifier_matches_attack_tags(modifier: Resource, attack_tags: Array[StringName]) -> bool:
|
||||
var required_tags := _string_name_array(modifier.get("required_attack_tags"))
|
||||
return required_tags.is_empty() or _has_any(attack_tags, required_tags)
|
||||
|
||||
|
||||
static func _string_name_array(value: Variant) -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
if value is Array:
|
||||
for item: Variant in value:
|
||||
result.append(StringName(str(item)))
|
||||
return result
|
||||
|
||||
|
||||
static func _has_any(values: Array[StringName], candidates: Array[StringName]) -> bool:
|
||||
for value: StringName in values:
|
||||
if candidates.has(value):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _stronger_defense_state(current_state: StringName, candidate_state: StringName) -> StringName:
|
||||
return candidate_state if _defense_priority(candidate_state) > _defense_priority(current_state) else current_state
|
||||
|
||||
|
||||
static func _defense_priority(defense_state: StringName) -> int:
|
||||
match defense_state:
|
||||
&"Invincible":
|
||||
return 4
|
||||
&"Parrying":
|
||||
return 3
|
||||
&"SuperArmor":
|
||||
return 2
|
||||
&"Vulnerable":
|
||||
return 1
|
||||
_:
|
||||
return 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0jjx3wiwtj2i
|
||||
@@ -0,0 +1,129 @@
|
||||
class_name StatResolver
|
||||
extends RefCounted
|
||||
|
||||
const JudgementPolicyResource := preload("res://resources/judgement_policy.tres")
|
||||
|
||||
|
||||
static func resolve_damage(base_attack: float, action: Resource, judgement: Variant = {}, buffs: Variant = null, burst: Variant = null) -> float:
|
||||
var judgement_dict := _as_dict(judgement)
|
||||
var action_mult := _resource_float(action, "damage_mult", 1.0)
|
||||
var judgement_mult := _judgement_channel_mult(judgement_dict, &"damage", "damage_mult", 1.0)
|
||||
var buff_mult := _provider_mult(buffs, "damage_mult", action)
|
||||
var burst_mult := _provider_mult(burst, "damage_mult", action)
|
||||
return base_attack * action_mult * judgement_mult * buff_mult * burst_mult
|
||||
|
||||
|
||||
static func resolve_cost(action: Resource, judgement: Variant = {}, provider: Variant = null) -> float:
|
||||
var resolved_provider: Variant = provider
|
||||
var judgement_dict := _as_dict(judgement)
|
||||
if not (judgement is Dictionary) and provider == null:
|
||||
resolved_provider = judgement
|
||||
var base_cost := _resource_float(action, "base_cost", 0.0)
|
||||
var judgement_mult := _judgement_channel_mult(judgement_dict, &"cost", "cost_mult", 1.0)
|
||||
var provider_mult := _provider_mult(resolved_provider, "cost_mult", action)
|
||||
return maxf(0.0, base_cost * judgement_mult * provider_mult)
|
||||
|
||||
|
||||
static func resolve_reward(action: Resource, judgement: Variant = {}, provider: Variant = null) -> float:
|
||||
var judgement_dict := _as_dict(judgement)
|
||||
var reward := _resource_float(action, "base_reward", 0.0)
|
||||
var judgement_mult := _judgement_channel_mult(judgement_dict, &"reward", "reward_mult", 1.0)
|
||||
var provider_mult := _provider_mult(provider, "reward_mult", action)
|
||||
return maxf(0.0, reward * judgement_mult * provider_mult)
|
||||
|
||||
|
||||
static func resolve_move(action: Resource, judgement: Variant = {}, burst: Variant = null) -> Vector2:
|
||||
var judgement_dict := _as_dict(judgement)
|
||||
var judgement_mult := _dict_float(judgement_dict, "move_mult", 1.0)
|
||||
var burst_mult := _provider_mult(burst, "move_mult", action)
|
||||
return Vector2(
|
||||
_resource_float(action, "move_mult_x", 0.0),
|
||||
_resource_float(action, "move_mult_y", 0.0)
|
||||
) * judgement_mult * burst_mult
|
||||
|
||||
|
||||
static func resolve_knockback(action: Resource, judgement: Variant, base_knockback: Vector2, provider: Variant = null) -> Vector2:
|
||||
var judgement_dict := _as_dict(judgement)
|
||||
# 击退跟随伤害判定通道(good 0.85 / bad 0.7),可被 judgement 里的显式
|
||||
# knockback_mult 覆盖 —— 与 test_combat_manager_resolvers 的契约一致。
|
||||
var judgement_mult := _judgement_channel_mult(judgement_dict, &"damage", "knockback_mult", 1.0)
|
||||
var provider_mult := _provider_mult(provider, "knockback_mult", action)
|
||||
return Vector2(
|
||||
base_knockback.x * _resource_float(action, "knockback_mult_x", 0.0),
|
||||
base_knockback.y * _resource_float(action, "knockback_mult_y", 0.0)
|
||||
) * judgement_mult * provider_mult
|
||||
|
||||
|
||||
static func resolve_action_snapshot(action: Resource, judgement: Variant = {}, provider: Variant = null) -> Dictionary:
|
||||
var resolved_provider: Variant = provider
|
||||
var judgement_dict := _as_dict(judgement)
|
||||
if not (judgement is Dictionary) and provider == null:
|
||||
resolved_provider = judgement
|
||||
return {
|
||||
"cost": resolve_cost(action, judgement_dict, resolved_provider),
|
||||
"reward": resolve_reward(action, judgement_dict, resolved_provider),
|
||||
"move": resolve_move(action, judgement_dict, resolved_provider),
|
||||
"startup_beats": _resolved_stat(action, "startup_beats", resolved_provider, 0.0),
|
||||
"active_beats": _resolved_stat(action, "active_beats", resolved_provider, 0.0),
|
||||
"recovery_beats": _resolved_stat(action, "recovery_beats", resolved_provider, 0.0),
|
||||
}
|
||||
|
||||
|
||||
static func _judgement_channel_mult(judgement: Dictionary, channel: StringName, override_key: String, fallback: float) -> float:
|
||||
if _dict_has(judgement, override_key):
|
||||
return _dict_float(judgement, override_key, fallback)
|
||||
var label := _judgement_label(judgement)
|
||||
if label.is_empty() or JudgementPolicyResource == null or not JudgementPolicyResource.has_method("multiplier"):
|
||||
return fallback
|
||||
return float(JudgementPolicyResource.call("multiplier", label, channel, fallback))
|
||||
|
||||
|
||||
static func _resource_float(resource: Resource, property_name: String, fallback: float) -> float:
|
||||
if resource == null:
|
||||
return fallback
|
||||
var value = resource.get(property_name)
|
||||
if value == null:
|
||||
return fallback
|
||||
return float(value)
|
||||
|
||||
|
||||
static func _dict_float(values: Dictionary, key: String, fallback: float) -> float:
|
||||
if values.has(key):
|
||||
return float(values[key])
|
||||
var name_key := StringName(key)
|
||||
if values.has(name_key):
|
||||
return float(values[name_key])
|
||||
return fallback
|
||||
|
||||
|
||||
static func _dict_has(values: Dictionary, key: String) -> bool:
|
||||
return values.has(key) or values.has(StringName(key))
|
||||
|
||||
|
||||
static func _judgement_label(judgement: Dictionary) -> StringName:
|
||||
if judgement.has("label"):
|
||||
return StringName(str(judgement["label"]).to_lower())
|
||||
if judgement.has(&"label"):
|
||||
return StringName(str(judgement[&"label"]).to_lower())
|
||||
return &""
|
||||
|
||||
|
||||
static func _as_dict(value: Variant) -> Dictionary:
|
||||
if value is Dictionary:
|
||||
return value
|
||||
return {}
|
||||
|
||||
|
||||
static func _resolved_stat(action: Resource, property_name: String, provider: Variant, fallback: float) -> float:
|
||||
var base_value := _resource_float(action, property_name, fallback)
|
||||
return maxf(0.0, base_value * _provider_mult(provider, property_name, action))
|
||||
|
||||
|
||||
static func _provider_mult(provider: Variant, method_name: String, action: Resource) -> float:
|
||||
if provider == null:
|
||||
return 1.0
|
||||
if provider.has_method(method_name):
|
||||
return float(provider.call(method_name, action))
|
||||
if provider.has_method("stat_multiplier"):
|
||||
return float(provider.call("stat_multiplier", StringName(method_name), action))
|
||||
return 1.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bhi2kegtvcxi6
|
||||
Reference in New Issue
Block a user