203 lines
7.5 KiB
GDScript
203 lines
7.5 KiB
GDScript
extends Area2D
|
|
|
|
const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn")
|
|
|
|
var direction := Vector2.UP
|
|
var projectile_speed := 600.0
|
|
var damage := 1.0
|
|
var max_range := 800.0
|
|
var weapon_kind := ItemDefinition.WeaponKind.PLASMA
|
|
var homing_target: Node2D
|
|
var homing_turn_speed := 3.2
|
|
var target_acquisition_range := 360.0
|
|
var _distance_travelled := 0.0
|
|
var _already_hit: Dictionary = {}
|
|
var _impact_handled := false
|
|
var _engine_time := 0.0
|
|
var _smoke_emission_timer := 0.0
|
|
var _smoke_puffs: Array[Dictionary] = []
|
|
|
|
func _ready() -> void:
|
|
body_entered.connect(_on_body_entered)
|
|
area_entered.connect(_on_area_entered)
|
|
rotation = direction.angle()
|
|
|
|
func configure(
|
|
start_position: Vector2,
|
|
travel_direction: Vector2,
|
|
kind: int,
|
|
speed: float,
|
|
projectile_damage: float,
|
|
projectile_range: float,
|
|
target: Node2D = null
|
|
) -> void:
|
|
global_position = start_position
|
|
direction = travel_direction.normalized()
|
|
weapon_kind = kind
|
|
projectile_speed = speed
|
|
damage = projectile_damage
|
|
max_range = projectile_range
|
|
homing_target = target
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if weapon_kind == ItemDefinition.WeaponKind.MISSILE:
|
|
if not is_instance_valid(homing_target):
|
|
homing_target = _find_enemy_in_acquisition_range()
|
|
if is_instance_valid(homing_target):
|
|
var to_target := homing_target.global_position - global_position
|
|
if to_target.length_squared() > 0.001:
|
|
var target_angle := to_target.angle()
|
|
direction = Vector2.RIGHT.rotated(rotate_toward(
|
|
direction.angle(), target_angle, homing_turn_speed * delta
|
|
))
|
|
rotation = direction.angle()
|
|
var step := projectile_speed * delta
|
|
var previous_position := global_position
|
|
var next_position := previous_position + direction * step
|
|
var query := PhysicsRayQueryParameters2D.create(
|
|
previous_position,
|
|
next_position,
|
|
collision_mask,
|
|
[get_rid()]
|
|
)
|
|
var result := get_world_2d().direct_space_state.intersect_ray(query)
|
|
if not result.is_empty():
|
|
global_position = result["position"]
|
|
_hit_target(result.get("collider"))
|
|
return
|
|
global_position = next_position
|
|
_distance_travelled += step
|
|
if _distance_travelled >= max_range:
|
|
queue_free()
|
|
|
|
func _process(delta: float) -> void:
|
|
if weapon_kind == ItemDefinition.WeaponKind.MISSILE:
|
|
_engine_time += delta
|
|
_smoke_emission_timer += delta
|
|
while _smoke_emission_timer >= 0.028:
|
|
_smoke_emission_timer -= 0.028
|
|
_spawn_smoke_puff()
|
|
_update_smoke_puffs(delta)
|
|
queue_redraw()
|
|
|
|
func _spawn_smoke_puff() -> void:
|
|
if _smoke_puffs.size() >= 64:
|
|
return
|
|
var side := Vector2(-direction.y, direction.x)
|
|
_smoke_puffs.append({
|
|
"position": global_position - direction * 8.0,
|
|
"velocity": direction * projectile_speed * 0.12 + side * randf_range(-9.0, 9.0),
|
|
"age": 0.0,
|
|
"life": randf_range(0.45, 0.75),
|
|
"size": randf_range(3.0, 5.0),
|
|
})
|
|
|
|
func _update_smoke_puffs(delta: float) -> void:
|
|
for puff_index in range(_smoke_puffs.size() - 1, -1, -1):
|
|
var puff := _smoke_puffs[puff_index]
|
|
puff["position"] = puff["position"] + puff["velocity"] * delta
|
|
puff["age"] = float(puff["age"]) + delta
|
|
_smoke_puffs[puff_index] = puff
|
|
if float(puff["age"]) >= float(puff["life"]):
|
|
_smoke_puffs.remove_at(puff_index)
|
|
|
|
func _find_enemy_in_acquisition_range() -> Node2D:
|
|
var nearest_enemy: Node2D
|
|
var nearest_distance_squared := target_acquisition_range * target_acquisition_range
|
|
for candidate in get_tree().get_nodes_in_group("enemies"):
|
|
if not candidate is Node2D or candidate.is_queued_for_deletion():
|
|
continue
|
|
var distance_squared := global_position.distance_squared_to(candidate.global_position)
|
|
if distance_squared <= nearest_distance_squared:
|
|
nearest_distance_squared = distance_squared
|
|
nearest_enemy = candidate
|
|
return nearest_enemy
|
|
|
|
func _draw() -> void:
|
|
if weapon_kind == ItemDefinition.WeaponKind.MISSILE:
|
|
_draw_missile_smoke()
|
|
match weapon_kind:
|
|
ItemDefinition.WeaponKind.PLASMA:
|
|
draw_circle(Vector2.ZERO, 6.0, Color(0.3, 0.85, 1.0, 0.95))
|
|
draw_circle(Vector2.ZERO, 3.0, Color(0.85, 0.98, 1.0, 1.0))
|
|
ItemDefinition.WeaponKind.MACHINE_GUN:
|
|
draw_rect(Rect2(-7.0, -2.0, 14.0, 4.0), Color(1.0, 0.82, 0.38, 1.0))
|
|
ItemDefinition.WeaponKind.MISSILE:
|
|
_draw_missile()
|
|
|
|
func _draw_missile_smoke() -> void:
|
|
for puff in _smoke_puffs:
|
|
var progress := clampf(float(puff["age"]) / float(puff["life"]), 0.0, 1.0)
|
|
var fade := 1.0 - progress
|
|
var puff_position: Vector2 = to_local(puff["position"])
|
|
var puff_size := float(puff["size"]) * (0.8 + progress * 0.75)
|
|
draw_circle(puff_position, puff_size, Color(0.46, 0.58, 0.67, fade * 0.2))
|
|
draw_circle(puff_position, puff_size * 0.48, Color(0.67, 0.75, 0.8, fade * 0.12))
|
|
|
|
func _draw_missile() -> void:
|
|
var flicker := sin(_engine_time * 37.0) * 2.5 + sin(_engine_time * 23.0) * 1.5
|
|
var flame_tip := -20.0 - flicker
|
|
# Layered exhaust uses tapered strokes so it flickers without polygon artifacts.
|
|
draw_line(Vector2(-8, 0), Vector2(flame_tip, 0), Color(1.0, 0.27, 0.045, 0.8), 6.0, true)
|
|
draw_line(Vector2(-8, 0), Vector2(flame_tip * 0.72, 0), Color(1.0, 0.78, 0.22, 0.95), 3.4, true)
|
|
draw_line(Vector2(-8, 0), Vector2(flame_tip * 0.42, 0), Color(0.92, 0.98, 1.0, 1.0), 1.6, true)
|
|
|
|
# Stabilizing fins and metallic body, facing along the local +X axis.
|
|
draw_line(Vector2(-5, -2.5), Vector2(-11, -7), Color(0.68, 0.17, 0.12, 1.0), 2.2, true)
|
|
draw_line(Vector2(-11, -7), Vector2(-9, -1.8), Color(0.68, 0.17, 0.12, 1.0), 2.2, true)
|
|
draw_line(Vector2(-5, 2.5), Vector2(-11, 7), Color(0.68, 0.17, 0.12, 1.0), 2.2, true)
|
|
draw_line(Vector2(-11, 7), Vector2(-9, 1.8), Color(0.68, 0.17, 0.12, 1.0), 2.2, true)
|
|
draw_rect(Rect2(-8, -3.5, 14, 7), Color(0.72, 0.79, 0.84, 1.0), true)
|
|
draw_rect(Rect2(3, -2.2, 3, 4.4), Color(0.91, 0.34, 0.2, 1.0), true)
|
|
draw_line(Vector2(6, -3.5), Vector2(12, 0), Color(0.91, 0.34, 0.2, 1.0), 2.6, true)
|
|
draw_line(Vector2(12, 0), Vector2(6, 3.5), Color(0.91, 0.34, 0.2, 1.0), 2.6, true)
|
|
draw_line(Vector2(-5, -2.4), Vector2(2, -2.4), Color(0.98, 0.99, 1.0, 0.85), 1.0, true)
|
|
draw_rect(Rect2(-9.5, -1.1, 2.0, 2.2), Color(0.25, 0.31, 0.36, 1.0))
|
|
draw_circle(Vector2(1.0, 0), 1.35, Color(0.62, 0.93, 1.0, 1.0))
|
|
|
|
func _on_body_entered(body: Node2D) -> void:
|
|
_hit_target(body)
|
|
|
|
func _on_area_entered(area: Area2D) -> void:
|
|
_hit_target(area)
|
|
|
|
func _hit_target(target: Node) -> void:
|
|
if _impact_handled:
|
|
return
|
|
_impact_handled = true
|
|
var is_explosion := weapon_kind == ItemDefinition.WeaponKind.MISSILE
|
|
_spawn_impact_effect(is_explosion)
|
|
if weapon_kind == ItemDefinition.WeaponKind.MISSILE:
|
|
_explode_at_target(target)
|
|
else:
|
|
_apply_damage_once(target)
|
|
queue_free()
|
|
|
|
func _spawn_impact_effect(is_explosion: bool) -> void:
|
|
var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D
|
|
effect.global_position = global_position
|
|
get_parent().add_child(effect)
|
|
var effect_color := Color(1.0, 0.48, 0.22) if is_explosion else Color(1.0, 0.78, 0.38)
|
|
effect.call("configure", is_explosion, effect_color)
|
|
|
|
func _explode_at_target(impact_target: Node) -> void:
|
|
_apply_damage_once(impact_target)
|
|
var explosion_shape := CircleShape2D.new()
|
|
explosion_shape.radius = 120.0
|
|
var query := PhysicsShapeQueryParameters2D.new()
|
|
query.shape = explosion_shape
|
|
query.transform = Transform2D(0.0, global_position)
|
|
query.collision_mask = 4 # Enemy physics layer 3.
|
|
for result in get_world_2d().direct_space_state.intersect_shape(query, 32):
|
|
_apply_damage_once(result.get("collider"))
|
|
|
|
func _apply_damage_once(target: Variant) -> void:
|
|
if not (target is Node):
|
|
return
|
|
var instance_id: int = target.get_instance_id()
|
|
if _already_hit.has(instance_id):
|
|
return
|
|
_already_hit[instance_id] = true
|
|
if target.has_method("take_damage"):
|
|
target.call("take_damage", damage)
|