extends Node const PROJECTILE_SCENE := preload("res://scenes/game/weapon_projectile.tscn") const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn") const WEAPON_AUDIO_SCRIPT := preload("res://scripts/game/weapon_audio.gd") const RAY_COLLISION_MASK := 77 # World, enemies, weapon test targets and infected ships. @onready var ship: CharacterBody2D = get_parent() @onready var inventory: ShipInventory = ship.get_node("ShipInventory") var _shot_timers := [0.0, 0.0, 0.0] var _laser_pulse_timers := [0.0, 0.0, 0.0] var _laser_lines: Array[Line2D] = [] var _fire_buttons_down := [false, false, false] var _fire_just_pressed := [false, false, false] var _drag_fire_blocked := false var missile_cooldown_remaining := 0.0 var missile_cooldown_duration := 5.0 func _ready() -> void: _ensure_input_action(&"ship_fire_primary", MOUSE_BUTTON_LEFT) _ensure_input_action(&"ship_fire_secondary", MOUSE_BUTTON_RIGHT) _ensure_input_action(&"ship_fire_missile", MOUSE_BUTTON_MIDDLE) for slot_index in ShipInventory.WEAPON_SLOT_COUNT: var beam := Line2D.new() beam.set_script(preload("res://scripts/game/laser_beam_visual.gd")) beam.width = 4.0 beam.default_color = Color(0.35, 0.9, 1.0, 0.92) beam.z_index = 20 beam.visible = false ship.get_parent().call_deferred("add_child", beam) _laser_lines.append(beam) func _ensure_input_action(action_name: StringName, button: MouseButton) -> void: if not InputMap.has_action(action_name): InputMap.add_action(action_name, 0.5) var event := InputEventMouseButton.new() event.button_index = button if not InputMap.action_has_event(action_name, event): InputMap.action_add_event(action_name, event) func block_ui_mouse_input() -> void: _fire_buttons_down.fill(false) _fire_just_pressed.fill(false) _drag_fire_blocked = true func _unhandled_input(event: InputEvent) -> void: if not event is InputEventMouseButton: return if ship.get_viewport().gui_is_dragging() or _drag_fire_blocked: return var slot_index := -1 if event.button_index == MOUSE_BUTTON_MIDDLE: slot_index = 2 if event.button_index == MOUSE_BUTTON_LEFT: slot_index = 0 elif event.button_index == MOUSE_BUTTON_RIGHT: slot_index = 1 if slot_index < 0: return _fire_buttons_down[slot_index] = event.pressed if event.pressed: _fire_just_pressed[slot_index] = true func _physics_process(delta: float) -> void: missile_cooldown_remaining = maxf(0.0, missile_cooldown_remaining - delta) if ship.get_viewport().gui_is_dragging(): _drag_fire_blocked = true if _drag_fire_blocked: for index in ShipInventory.WEAPON_SLOT_COUNT: _fire_buttons_down[index] = false _fire_just_pressed[index] = false _laser_pulse_timers[index] = 0.0 _laser_lines[index].visible = false _shot_timers[index] = maxf(0.0, _shot_timers[index] - delta) # Require release after dropping, so the drag gesture cannot become a shot. if not ship.get_viewport().gui_is_dragging() and not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and not Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) and not Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE): _drag_fire_blocked = false return if bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")): for line in _laser_lines: line.visible = false return var mouse_over_ui := ship.get_viewport().gui_get_hovered_control() != null for slot_index in ShipInventory.WEAPON_SLOT_COUNT: _shot_timers[slot_index] = maxf(0.0, _shot_timers[slot_index] - delta) _laser_pulse_timers[slot_index] = maxf(0.0, _laser_pulse_timers[slot_index] - delta) var action_name: String = ["ship_fire_primary", "ship_fire_secondary", "ship_fire_missile"][slot_index] var is_pressed: bool = not mouse_over_ui and ( Input.is_action_pressed(action_name) or _fire_buttons_down[slot_index] ) var is_just_pressed: bool = not mouse_over_ui and ( Input.is_action_just_pressed(action_name) or _fire_just_pressed[slot_index] ) var item: ItemInstance = inventory.get_item("weapon",slot_index) if not is_pressed or item == null or item.definition == null: _laser_lines[slot_index].visible = ( item != null and item.definition != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.LASER and _laser_pulse_timers[slot_index] > 0.0 ) continue var definition := item.definition if definition.weapon_kind != ItemDefinition.WeaponKind.LASER: _laser_pulse_timers[slot_index] = 0.0 _laser_lines[slot_index].visible = false match definition.weapon_kind: ItemDefinition.WeaponKind.LASER: _laser_lines[slot_index].visible = _laser_pulse_timers[slot_index] > 0.0 _fire_laser(slot_index, item) ItemDefinition.WeaponKind.MISSILE: if is_just_pressed: try_fire_missile(slot_index) _: if _shot_timers[slot_index] <= 0.0 and _consume_shot_resource(item): _fire_projectile(item) _shot_timers[slot_index] = definition.fire_interval for slot_index in ShipInventory.WEAPON_SLOT_COUNT: _fire_just_pressed[slot_index] = false func try_fire_missile(slot_index: int = 2) -> bool: if missile_cooldown_remaining > 0.0 or bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")): return false var item := inventory.get_item("weapon", slot_index) if item == null or item.definition.weapon_kind != ItemDefinition.WeaponKind.MISSILE: return false if inventory.missile_ammunition <= 0: return false missile_cooldown_duration = inventory.get_missile_cooldown(item) _fire_projectile(item) missile_cooldown_remaining = missile_cooldown_duration inventory.consume_missile() return true func _fire_projectile(item: ItemInstance) -> void: _break_cloak_on_fire() var definition := item.definition var hit: Dictionary = ship.call("roll_weapon_hit", item) var muzzle_position := _muzzle_position() var direction := _aim_direction_from(muzzle_position) var projectile := PROJECTILE_SCENE.instantiate() projectile.call( "configure", muzzle_position, direction, definition.weapon_kind, definition.projectile_speed, float(hit["damage"]), definition.weapon_range, null, bool(hit["critical"]) ) ship.get_parent().add_child(projectile) projectile.reset_physics_interpolation() projectile.set("shield_piercing", item.shield_piercing()) projectile.set("missile_homing", definition.missile_homing) WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind) if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: var flash := Node2D.new() flash.set_script(preload("res://scripts/game/muzzle_flash.gd")) # Keep the short flash attached to the muzzle while the ship moves. flash.position = ship.to_local(muzzle_position) flash.rotation = direction.angle() - ship.global_rotation ship.add_child(flash) flash.reset_physics_interpolation() func _exit_tree() -> void: for beam in _laser_lines: if is_instance_valid(beam): beam.queue_free() func _fire_laser(slot_index: int, item: ItemInstance) -> void: var definition := item.definition var line := _laser_lines[slot_index] if _shot_timers[slot_index] > 0.0 or float(ship.get("energy")) < item.get_energy_cost(): return var muzzle_position := _muzzle_position() var direction := _aim_direction_from(muzzle_position) var endpoint := muzzle_position + direction * definition.weapon_range var query := PhysicsRayQueryParameters2D.create( muzzle_position, endpoint, RAY_COLLISION_MASK, [ship.get_rid()] ) var result := ship.get_world_2d().direct_space_state.intersect_ray(query) if not result.is_empty(): endpoint = result["position"] line.clear_points() line.add_point(muzzle_position) line.add_point(endpoint) line.visible = true if not _consume_energy(item.get_energy_cost()): line.visible = false return WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, ItemDefinition.WeaponKind.LASER) _break_cloak_on_fire() if not result.is_empty(): var hit: Dictionary = ship.call("roll_weapon_hit", item) _apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]), item.shield_piercing()) _spawn_laser_impact(endpoint) _laser_pulse_timers[slot_index] = definition.laser_pulse_duration _shot_timers[slot_index] = definition.fire_interval func _break_cloak_on_fire() -> void: ship.landing_combat_lock = 6.0 var systems := ship.get_node_or_null("ExtensionSystems") if systems != null: systems.call("break_cloak_on_fire") func _aim_direction_from(origin: Vector2) -> Vector2: var offset := ship.get_global_mouse_position() - origin return offset.normalized() if offset.length_squared() > 0.001 else Vector2.UP.rotated(ship.rotation) func _muzzle_position() -> Vector2: var forward := Vector2.UP.rotated(ship.rotation) return ship.global_position + forward * 28.0 func _consume_shot_resource(item: ItemInstance) -> bool: if item.definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: return inventory.consume_ammunition() if item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: return inventory.consume_irradiated_ammunition() return _consume_energy(item.get_energy_cost()) func _consume_energy(amount: float) -> bool: var current_energy := float(ship.get("energy")) if current_energy < amount: return false ship.set("energy", current_energy - amount) return true func _apply_damage(target: Variant, damage: float, critical: bool = false, piercing: float = 0.0) -> void: if target is Node and target.has_method("take_piercing_weapon_hit"): target.call("take_piercing_weapon_hit", damage, critical, DamageTypes.Type.ENERGY, piercing) elif target is Node and target.has_method("take_weapon_hit"): target.call("take_weapon_hit", damage, critical, DamageTypes.Type.ENERGY) elif target is Node and target.has_method("take_damage"): target.call("take_damage", damage) func _spawn_laser_impact(at_position: Vector2) -> void: var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D effect.global_position = at_position ship.get_parent().add_child(effect) effect.call("configure", false, Color(0.38, 0.9, 1.0))