class_name VesselWeaponComponent extends Node # The fleet's gun. It does NOT pick its own targets: the player aims it with # the fire stick, and it shoots along whatever heading that stick reports for # as long as it is held. # # It used to seek the nearest enemy in range and fire on its own, which meant # combat happened whether or not the player did anything - there was nothing # to aim, and nothing to decide. Firing is now an action like any other, and # the right-hand stick is what performs it. # # `fire_direction` is set every frame by FireStickController from the stick's # own reading, and cleared the moment it is released. Zero means "not firing", # which is the resting state. var capability_id: String = "weapon" var fire_range: float var fire_cooldown: float var projectile_damage: int var fire_timer: float = 0.0 var fire_direction: Vector2 = Vector2.ZERO var projectile_scene: PackedScene = preload("res://vessels/Projectile.tscn") # By path, not by class name - see the note at the top of MuzzleSparkVFX. const MUZZLE_SPARK = preload("res://vfx/MuzzleSparkVFX.gd") func apply_stats() -> void: var ship: Node = get_parent() var state: Node = ship.get_tree().root.get_node("GameState") fire_range = state.vessel_stat(ship.vessel_type, "fire_range") fire_cooldown = state.vessel_stat(ship.vessel_type, "fire_cooldown") projectile_damage = int(state.vessel_stat(ship.vessel_type, "damage")) # Forgejo #89: how far off the aimed heading a target may sit and still be # picked up. 28 degrees is wide enough that pointing roughly at something hits # it, and narrow enough that the shot still goes where the player pointed - # past about 45 the gun starts choosing targets on its own, which is the # behaviour #80 deliberately took away. const ASSIST_CONE_DEGREES: float = 28.0 # The heading a shot fired from `origin` along `direction` should actually # take: straight, unless an enemy sits inside the assist cone and within # reach, in which case it points at the nearest one. # # Static, and used by BOTH the gun and the aim guide (see # FireStickController._draw). If the guide drew the raw stick heading while # the shot took an assisted one, the line would be telling the player # something the gun does not do - the same class of mismatch #86 removed # between the guide's length and a bolt's actual reach. # # Enemies only, never asteroids. Snapping onto a rock while the player is # trying to hit the thing shooting at them would be the assist working # against them; rocks are large, stationary enough, and easy to hit on # purpose. static func assisted_heading(tree: SceneTree, origin: Vector2, direction: Vector2, reach: float) -> Vector2: var heading: Vector2 = direction.normalized() if heading == Vector2.ZERO or tree == null: return heading var cone: float = cos(deg_to_rad(ASSIST_CONE_DEGREES)) var nearest: Vector2 = Vector2.ZERO var nearest_distance: float = INF for enemy in tree.get_nodes_in_group("enemies"): if not is_instance_valid(enemy) or not (enemy is Node2D): continue var to_enemy: Vector2 = (enemy as Node2D).global_position - origin var distance: float = to_enemy.length() if distance > reach or distance < 0.001 or distance >= nearest_distance: continue if to_enemy.normalized().dot(heading) < cone: continue nearest_distance = distance nearest = to_enemy return heading if nearest == Vector2.ZERO else nearest.normalized() # Called by FireStickController. A zero vector stops the gun. func set_fire_direction(direction: Vector2) -> void: fire_direction = direction if direction.length() > 0.001 else Vector2.ZERO func is_firing() -> bool: return fire_direction != Vector2.ZERO func tick(delta: float) -> void: # The cooldown runs down whether or not the trigger is held, so tapping # the stick in a new direction is not punished by a timer that only # started when the stick moved. fire_timer -= delta if fire_direction == Vector2.ZERO or fire_timer > 0.0: return var ship: Node2D = get_parent() var projectile: Node2D = projectile_scene.instantiate() projectile.position = ship.position projectile.damage = projectile_damage # The bolt stops where the gun's range ends, and the aim guide draws that # same range - one number, one meaning. projectile.set_range(fire_range) ship.get_tree().root.add_child(projectile) # Aimed by DIRECTION, not at a target: launch() takes a displacement, so # the gun's own range decides how far the shot is sent. var heading: Vector2 = assisted_heading(ship.get_tree(), ship.global_position, fire_direction, fire_range) projectile.launch(heading * fire_range) # #87: sparks off the hull that fired, so a shot is visibly attached to # the vessel it came from. MUZZLE_SPARK.spawn(ship.get_tree().root, ship.global_position, heading) # #146: and heard. Beside the spark rather than inside it, because the # spark is about this hull and the sound is about the shot. Sfx.play(ship.get_tree(), Sfx.SHOT) fire_timer = fire_cooldown