Initial project sync
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user