Initial project sync
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnc3bixik3wxn
|
||||
@@ -0,0 +1,74 @@
|
||||
class_name ContactFxSpawner
|
||||
extends Node
|
||||
|
||||
const OneShotFxScript := preload("res://scenes/combat/one_shot_fx.gd")
|
||||
|
||||
const CONTACT_SPECS := {
|
||||
&"basic_1": {"path": "res://assets/art/characters/player/01_basic_attack_i/enemy_contact_fx.png", "hframes": 7, "vframes": 1, "first": 0, "last": 6, "fps": 20.0},
|
||||
&"basic_2": {"path": "res://assets/art/characters/player/02_basic_attack_ii/enemy_contact_fx.png", "hframes": 7, "vframes": 1, "first": 0, "last": 6, "fps": 20.0},
|
||||
&"basic_3": {"path": "res://assets/art/characters/player/03_basic_attack_iii/enemy_contact_fx.png", "hframes": 7, "vframes": 1, "first": 0, "last": 6, "fps": 20.0},
|
||||
&"finisher": {"path": "res://assets/art/characters/player/05_combo_finisher/enemy_contact_fx.png", "hframes": 7, "vframes": 1, "first": 0, "last": 6, "fps": 20.0},
|
||||
&"projectile": {"path": "res://assets/art/characters/player/11_blade_wave/projectile_enemy_contact.png", "hframes": 5, "vframes": 1, "first": 0, "last": 4, "fps": 20.0, "scale": 0.7},
|
||||
}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null and bus.has_signal("hit_confirmed") and not bus.is_connected("hit_confirmed", _on_hit_confirmed):
|
||||
bus.connect("hit_confirmed", _on_hit_confirmed)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null and bus.has_signal("hit_confirmed") and bus.is_connected("hit_confirmed", _on_hit_confirmed):
|
||||
bus.disconnect("hit_confirmed", _on_hit_confirmed)
|
||||
|
||||
|
||||
func _on_hit_confirmed(result: Dictionary) -> void:
|
||||
if int(result.get("damage", 0)) <= 0:
|
||||
return
|
||||
var emitter := result.get("emitter", null) as Area2D
|
||||
var receiver := result.get("receiver", null) as Area2D
|
||||
if emitter == null or receiver == null or not is_instance_valid(emitter) or not is_instance_valid(receiver):
|
||||
return
|
||||
if not _is_our_hit(emitter):
|
||||
return
|
||||
var target := receiver.get_parent() as Node2D
|
||||
if target == null:
|
||||
return
|
||||
var spec: Dictionary = CONTACT_SPECS.get(_spec_key_for(result), {})
|
||||
if spec.is_empty():
|
||||
return
|
||||
var config := spec.duplicate()
|
||||
var from := result.get("from", target.global_position) as Vector2
|
||||
config["flip_h"] = from.x > target.global_position.x
|
||||
OneShotFxScript.spawn(target, target.global_position + Vector2(0.0, -70.0), config)
|
||||
|
||||
|
||||
func _is_our_hit(emitter: Area2D) -> bool:
|
||||
var actor := get_parent()
|
||||
if actor != null and emitter.get_parent() == actor:
|
||||
return true
|
||||
return emitter.is_in_group("player_projectiles")
|
||||
|
||||
|
||||
func _spec_key_for(result: Dictionary) -> StringName:
|
||||
if StringName(str(result.get("hit_type", &""))) == &"projectile":
|
||||
return &"projectile"
|
||||
var action: Resource = result.get("action", null) as Resource
|
||||
if action == null:
|
||||
return &"basic_1"
|
||||
var action_id := str(action.get("id"))
|
||||
if action_id.begins_with("ground_attack") and action_id.ends_with("_2"):
|
||||
return &"basic_2"
|
||||
if action_id.begins_with("ground_attack") and action_id.ends_with("_3"):
|
||||
return &"basic_3"
|
||||
if action_id.begins_with("combo_finisher") or action_id.begins_with("ground_smash"):
|
||||
return &"finisher"
|
||||
return &"basic_1"
|
||||
|
||||
|
||||
func _event_bus_or_null() -> Node:
|
||||
if not is_inside_tree():
|
||||
return null
|
||||
return get_tree().root.get_node_or_null("EventBus")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dl4cd5x3uc0bl
|
||||
@@ -0,0 +1,81 @@
|
||||
class_name OneShotFx
|
||||
extends Node2D
|
||||
|
||||
var texture: Texture2D
|
||||
var hframes := 1
|
||||
var vframes := 1
|
||||
var first_frame := 0
|
||||
var last_frame := 0
|
||||
var fps := 20.0
|
||||
var delay := 0.0
|
||||
var duration_override := 0.0
|
||||
var velocity := Vector2.ZERO
|
||||
var flip_h := false
|
||||
var sprite_scale := Vector2.ONE
|
||||
var sprite_offset := Vector2.ZERO
|
||||
|
||||
var _sprite: Sprite2D
|
||||
var _elapsed := 0.0
|
||||
|
||||
|
||||
static func spawn(parent: Node, spawn_global_position: Vector2, config: Dictionary) -> Node2D:
|
||||
if parent == null:
|
||||
return null
|
||||
var script: Script = load("res://scenes/combat/one_shot_fx.gd")
|
||||
var fx: Node2D = script.new()
|
||||
var texture_path := str(config.get("path", ""))
|
||||
if ResourceLoader.exists(texture_path):
|
||||
fx.set("texture", load(texture_path))
|
||||
fx.set("hframes", maxi(1, int(config.get("hframes", 1))))
|
||||
fx.set("vframes", maxi(1, int(config.get("vframes", 1))))
|
||||
fx.set("first_frame", maxi(0, int(config.get("first", 0))))
|
||||
fx.set("last_frame", maxi(0, int(config.get("last", 0))))
|
||||
fx.set("fps", maxf(1.0, float(config.get("fps", 20.0))))
|
||||
fx.set("delay", maxf(0.0, float(config.get("delay", 0.0))))
|
||||
fx.set("duration_override", maxf(0.0, float(config.get("duration", 0.0))))
|
||||
fx.set("velocity", config.get("velocity", Vector2.ZERO))
|
||||
fx.set("flip_h", bool(config.get("flip_h", false)))
|
||||
var scale_value := float(config.get("scale", 1.0))
|
||||
fx.set("sprite_scale", Vector2(scale_value, scale_value))
|
||||
fx.set("sprite_offset", config.get("offset", Vector2.ZERO))
|
||||
fx.z_index = int(config.get("z_index", 3))
|
||||
parent.add_child(fx)
|
||||
fx.global_position = spawn_global_position
|
||||
return fx
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if texture == null:
|
||||
queue_free()
|
||||
return
|
||||
_sprite = Sprite2D.new()
|
||||
_sprite.texture = texture
|
||||
_sprite.hframes = hframes
|
||||
_sprite.vframes = vframes
|
||||
_sprite.centered = true
|
||||
_sprite.scale = sprite_scale
|
||||
_sprite.offset = sprite_offset
|
||||
_sprite.flip_h = flip_h
|
||||
_sprite.frame = clampi(first_frame, 0, hframes * vframes - 1)
|
||||
_sprite.visible = delay <= 0.0
|
||||
add_child(_sprite)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_elapsed += delta
|
||||
if _elapsed < delay:
|
||||
return
|
||||
if _sprite == null:
|
||||
queue_free()
|
||||
return
|
||||
_sprite.visible = true
|
||||
var play_time := _elapsed - delay
|
||||
position += velocity * delta
|
||||
var lifetime := duration_override
|
||||
if lifetime <= 0.0:
|
||||
lifetime = float(last_frame - first_frame + 1) / fps
|
||||
if play_time >= lifetime:
|
||||
queue_free()
|
||||
return
|
||||
var frame_index := first_frame + int(play_time * fps)
|
||||
_sprite.frame = clampi(frame_index, first_frame, mini(last_frame, hframes * vframes - 1))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi8tnfsrabjok
|
||||
@@ -0,0 +1,190 @@
|
||||
class_name PlayerProjectile
|
||||
extends Area2D
|
||||
|
||||
const EFFECT_TEXTURE := preload("res://assets/art/effects/effect_sheet.png")
|
||||
# effect_sheet.png is a 6x2 grid of 32x32 cells; row one holds four flight frames
|
||||
# (cells four and five are empty), row two holds the impact burst.
|
||||
const SHEET_HFRAMES := 6
|
||||
const SHEET_VFRAMES := 2
|
||||
const FLIGHT_FRAME_COUNT := 4
|
||||
const FLIGHT_FPS := 16.0
|
||||
const VANISH_FRAME := 4
|
||||
const HIT_LEAD_DISTANCE := 18.0
|
||||
const DEFAULT_MAX_RANGE := 460.0
|
||||
|
||||
# The player blade wave uses the author's PROJECTILE sheet (5 frames, 33fps loop).
|
||||
const WAVE_TEXTURE_PATH := "res://assets/art/characters/player/11_blade_wave/projectile.png"
|
||||
# collision_radius keeps the hit circle inside the visible art: the effect_sheet
|
||||
# flight frames show a ~32x18px bolt at scale 2 (radius 9 = half its height),
|
||||
# the wave sheet's smallest frame is ~66x65px at scale 0.55 (radius 32 fits).
|
||||
const VISUAL_PROFILES := {
|
||||
&"bullet": {"hframes": SHEET_HFRAMES, "vframes": SHEET_VFRAMES, "first": 0, "last": FLIGHT_FRAME_COUNT - 1, "fps": FLIGHT_FPS, "scale": 2.0, "vanish_frame": VANISH_FRAME, "collision_radius": 9.0},
|
||||
&"wave": {"path": WAVE_TEXTURE_PATH, "hframes": 5, "vframes": 1, "first": 0, "last": 4, "fps": 33.0, "scale": 0.55, "vanish_frame": -1, "collision_radius": 32.0},
|
||||
}
|
||||
|
||||
@export var damage := 100
|
||||
@export var hit_type: StringName = &"projectile"
|
||||
@export var base_knockback := Vector2.ZERO
|
||||
|
||||
## Snapshotted when fired: weak-state enemy projectiles deal damage but do not
|
||||
## interrupt, while Boss/player projectiles keep full authority.
|
||||
var attacker_interrupts := true
|
||||
var source_actor: Node = null
|
||||
var action_context: Resource
|
||||
var judgement_context: Dictionary = {"label": "perfect"}
|
||||
var direction := Vector2.RIGHT
|
||||
var speed := 520.0
|
||||
var max_range := DEFAULT_MAX_RANGE
|
||||
var visual_profile: StringName = &"bullet"
|
||||
var _travelled := 0.0
|
||||
var _age := 0.0
|
||||
var _sprite: Sprite2D
|
||||
var _hit_confirmed := false
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_create_sprite()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if _sprite == null:
|
||||
_create_sprite()
|
||||
_apply_visual_profile()
|
||||
if not area_entered.is_connected(_on_area_entered):
|
||||
area_entered.connect(_on_area_entered)
|
||||
|
||||
|
||||
func _apply_visual_profile() -> void:
|
||||
if _sprite == null:
|
||||
return
|
||||
var profile := _visual()
|
||||
var texture_path := str(profile.get("path", ""))
|
||||
if not texture_path.is_empty() and ResourceLoader.exists(texture_path):
|
||||
_sprite.texture = load(texture_path)
|
||||
else:
|
||||
_sprite.texture = EFFECT_TEXTURE
|
||||
_sprite.hframes = maxi(1, int(profile.get("hframes", 1)))
|
||||
_sprite.vframes = maxi(1, int(profile.get("vframes", 1)))
|
||||
var profile_scale := float(profile.get("scale", 1.0))
|
||||
_sprite.scale = Vector2(profile_scale, profile_scale)
|
||||
_sprite.frame = clampi(int(profile.get("first", 0)), 0, _sprite.hframes * _sprite.vframes - 1)
|
||||
_apply_collision_profile(profile)
|
||||
|
||||
|
||||
func _apply_collision_profile(profile: Dictionary) -> void:
|
||||
var shape_node := get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shape_node == null or not (shape_node.shape is CircleShape2D):
|
||||
return
|
||||
# The scene's CircleShape2D is a shared subresource: duplicate before sizing
|
||||
# per profile so bullet and wave instances never overwrite each other.
|
||||
var circle := (shape_node.shape as CircleShape2D).duplicate() as CircleShape2D
|
||||
circle.radius = maxf(1.0, float(profile.get("collision_radius", circle.radius)))
|
||||
shape_node.shape = circle
|
||||
|
||||
|
||||
func _visual() -> Dictionary:
|
||||
return VISUAL_PROFILES.get(visual_profile, VISUAL_PROFILES[&"bullet"])
|
||||
|
||||
|
||||
func _create_sprite() -> void:
|
||||
if _sprite != null and is_instance_valid(_sprite):
|
||||
return
|
||||
_sprite = Sprite2D.new()
|
||||
_sprite.name = "Sprite"
|
||||
_sprite.texture = EFFECT_TEXTURE
|
||||
_sprite.hframes = SHEET_HFRAMES
|
||||
_sprite.vframes = SHEET_VFRAMES
|
||||
_sprite.centered = true
|
||||
_sprite.scale = Vector2(2.0, 2.0)
|
||||
add_child(_sprite)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_age += delta
|
||||
if not _hit_confirmed:
|
||||
var profile := _visual()
|
||||
var first := int(profile.get("first", 0))
|
||||
var span := maxi(1, int(profile.get("last", first)) - first + 1)
|
||||
_sprite.frame = first + int(_age * float(profile.get("fps", FLIGHT_FPS))) % span
|
||||
_sprite.rotation = direction.angle()
|
||||
_sprite.flip_h = false
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if _hit_confirmed:
|
||||
return
|
||||
var movement := direction.normalized() * speed * delta
|
||||
var receiver := _swept_receiver(movement)
|
||||
if receiver != null:
|
||||
_on_area_entered(receiver)
|
||||
return
|
||||
position += movement
|
||||
_travelled += movement.length()
|
||||
if _travelled >= max_travel_distance():
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_area_entered(receiver: Area2D) -> void:
|
||||
if _hit_confirmed:
|
||||
return
|
||||
if receiver.is_in_group("enemy_projectiles") and is_in_group("player_projectiles"):
|
||||
receiver.queue_free()
|
||||
return
|
||||
if not receiver.is_in_group("damage_receivers"):
|
||||
return
|
||||
_hit_confirmed = true
|
||||
_vanish_after_hit()
|
||||
var combat := _combat_manager_or_null()
|
||||
if combat != null and combat.has_method("resolve_hit"):
|
||||
combat.call("resolve_hit", self, receiver)
|
||||
queue_free()
|
||||
|
||||
|
||||
func _combat_manager_or_null() -> Node:
|
||||
if not is_inside_tree():
|
||||
return null
|
||||
return get_tree().root.get_node_or_null("CombatManager")
|
||||
|
||||
|
||||
func max_travel_distance() -> float:
|
||||
return max_range if max_range > 0.0 else DEFAULT_MAX_RANGE
|
||||
|
||||
|
||||
func _vanish_after_hit() -> void:
|
||||
set_deferred("monitoring", false)
|
||||
set_deferred("monitorable", false)
|
||||
visible = false
|
||||
var vanish_frame := int(_visual().get("vanish_frame", -1))
|
||||
if _sprite != null and vanish_frame >= 0:
|
||||
_sprite.frame = vanish_frame
|
||||
|
||||
|
||||
func _swept_receiver(movement: Vector2) -> Area2D:
|
||||
if movement == Vector2.ZERO or not is_inside_tree():
|
||||
return null
|
||||
var travel_direction := movement.normalized()
|
||||
var shape_node := get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shape_node != null and shape_node.shape != null:
|
||||
var shape_query := PhysicsShapeQueryParameters2D.new()
|
||||
shape_query.shape = shape_node.shape
|
||||
shape_query.transform = shape_node.global_transform.translated(travel_direction * HIT_LEAD_DISTANCE + movement)
|
||||
shape_query.collision_mask = collision_mask
|
||||
shape_query.collide_with_areas = true
|
||||
shape_query.collide_with_bodies = false
|
||||
shape_query.exclude = [get_rid()]
|
||||
for hit: Dictionary in get_world_2d().direct_space_state.intersect_shape(shape_query, 8):
|
||||
var shape_collider = hit.get("collider", null)
|
||||
if shape_collider is Area2D and shape_collider.is_in_group("damage_receivers"):
|
||||
return shape_collider
|
||||
var query := PhysicsRayQueryParameters2D.create(
|
||||
global_position + travel_direction * HIT_LEAD_DISTANCE,
|
||||
global_position + travel_direction * HIT_LEAD_DISTANCE + movement,
|
||||
collision_mask
|
||||
)
|
||||
query.collide_with_areas = true
|
||||
query.collide_with_bodies = false
|
||||
var hit := get_world_2d().direct_space_state.intersect_ray(query)
|
||||
var collider = hit.get("collider", null)
|
||||
if collider is Area2D and collider.is_in_group("damage_receivers"):
|
||||
return collider
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8ek2cytdwsmp
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/combat/player_projectile.gd" id="1"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_projectile"]
|
||||
radius = 32.0
|
||||
|
||||
[node name="PlayerProjectile" type="Area2D"]
|
||||
collision_layer = 16
|
||||
collision_mask = 2
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("CircleShape2D_projectile")
|
||||
Reference in New Issue
Block a user