325 lines
14 KiB
GDScript
325 lines
14 KiB
GDScript
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
|
|
var _last_hit_targets: Dictionary = {}
|
|
|
|
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_effect == "external_turret":
|
|
_laser_pulse_timers[slot_index] = 0.0
|
|
_laser_lines[slot_index].visible = false
|
|
if is_just_pressed: try_deploy_turret(slot_index)
|
|
continue
|
|
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] = item.get_fire_interval()
|
|
for slot_index in ShipInventory.WEAPON_SLOT_COUNT:
|
|
_fire_just_pressed[slot_index] = false
|
|
|
|
func try_deploy_turret(slot_index: int) -> bool:
|
|
if slot_index < 0 or slot_index >= ShipInventory.WEAPON_SLOT_COUNT or _shot_timers[slot_index] > 0.0:
|
|
return false
|
|
if get_tree().paused 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_effect != "external_turret" or inventory.ammunition < 50:
|
|
return false
|
|
var turret := Node2D.new()
|
|
turret.set_script(preload("res://scripts/game/deployed_turret.gd"))
|
|
turret.set("mother",ship)
|
|
turret.set("weapon",item.duplicate(true))
|
|
var direction := _aim_direction_from(ship.global_position)
|
|
turret.position = ship.global_position + direction * 70.0
|
|
turret.rotation = direction.angle() + PI * 0.5
|
|
inventory.ammunition -= 50
|
|
inventory.ammunition_changed.emit()
|
|
ship.get_parent().add_child(turret)
|
|
_shot_timers[slot_index] = item.get_fire_interval()
|
|
_break_cloak_on_fire()
|
|
var flash := Node2D.new()
|
|
flash.set_script(preload("res://scripts/game/deployment_flash.gd"))
|
|
flash.position = turret.position
|
|
flash.set("color",Color("70dfff"))
|
|
ship.get_parent().add_child(flash)
|
|
return true
|
|
|
|
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.weapon_effect = definition.weapon_effect
|
|
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.roll_shield_piercing())
|
|
if definition.weapon_effect == "pulse_laser": projectile.set("source_ship",ship)
|
|
projectile.set("damage_type_override", item.get_damage_type())
|
|
projectile.set("missile_homing", definition.missile_homing)
|
|
if definition.weapon_effect == "artillery":
|
|
preload("res://scripts/game/artillery_effect.gd").launch(ship,muzzle_position,direction)
|
|
else:
|
|
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind)
|
|
if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN and definition.weapon_effect != "artillery":
|
|
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 definition.weapon_effect == "pulse_laser":
|
|
line.visible = false
|
|
_laser_pulse_timers[slot_index] = 0.0
|
|
if _shot_timers[slot_index] <= 0.0 and _consume_energy(item.get_energy_cost()):
|
|
_fire_projectile(item)
|
|
_shot_timers[slot_index] = item.get_fire_interval()
|
|
return
|
|
line.set("radiation_beam",item.get_damage_type() == DamageTypes.Type.RADIATION)
|
|
if _shot_timers[slot_index] > 0.0 or float(ship.get("energy")) < _effective_energy_cost(item.get_energy_cost()):
|
|
return
|
|
var piercing := item.roll_shield_piercing()
|
|
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 piercing < 1.0:
|
|
result = preload("res://scripts/game/shield_intersections.gd").intercept(result,muzzle_position,endpoint,RAY_COLLISION_MASK,[ship.get_rid()],get_tree())
|
|
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)
|
|
var target: Node = result.get("collider")
|
|
var hull_before := float(target.get("hull")) if target.has_method("take_research_weapon_hit") else -1.0
|
|
preload("res://scripts/game/combat_vfx.gd").mark_hit(target,endpoint,direction)
|
|
_apply_damage(target, float(hit["damage"]), bool(hit["critical"]), piercing, item.get_damage_type())
|
|
_apply_laser_radiation(target,item,float(hit.damage))
|
|
_spawn_laser_impact(endpoint,item.get_damage_type(),direction)
|
|
if hull_before >= 0 and float(target.get("hull")) < hull_before and preload("res://scripts/planets/alien_research.gd").learned("hull_a_next_next"):
|
|
var visited := _last_hit_targets.duplicate()
|
|
visited[target.get_instance_id()] = true
|
|
var exclusions: Array[RID] = [ship.get_rid(),target.get_rid()]
|
|
for hit_id in visited:
|
|
var prior := instance_from_id(hit_id) as CollisionObject2D
|
|
if is_instance_valid(prior) and prior.get_rid() not in exclusions: exclusions.append(prior.get_rid())
|
|
var through := PhysicsRayQueryParameters2D.create(endpoint+direction*2.0,muzzle_position+direction*definition.weapon_range,RAY_COLLISION_MASK,exclusions)
|
|
var second := ship.get_world_2d().direct_space_state.intersect_ray(through)
|
|
if piercing < 1.0:
|
|
second = preload("res://scripts/game/shield_intersections.gd").intercept(second,endpoint+direction*2.0,muzzle_position+direction*definition.weapon_range,RAY_COLLISION_MASK,exclusions,get_tree())
|
|
var second_end: Vector2 = second.get("position",muzzle_position+direction*definition.weapon_range)
|
|
line.add_point(second_end)
|
|
if not second.is_empty():
|
|
preload("res://scripts/game/combat_vfx.gd").mark_hit(second.get("collider"),second_end,direction)
|
|
preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(second.get("collider"),float(hit.damage)*0.5,bool(hit.critical),item.get_damage_type(),piercing,visited,false,-1,0.5)
|
|
_apply_laser_radiation(second.get("collider"),item,float(hit.damage)*0.5)
|
|
_spawn_laser_impact(second_end,item.get_damage_type(),direction)
|
|
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration
|
|
_shot_timers[slot_index] = item.get_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 _apply_laser_radiation(target: Node, item: ItemInstance, damage: float) -> void:
|
|
if item.definition.weapon_effect == "radiation_laser" and item.get_damage_type() == DamageTypes.Type.RADIATION and not target.is_queued_for_deletion():
|
|
preload("res://scripts/game/radiation_status.gd").apply(target,damage,true,item.shield_piercing())
|
|
|
|
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(item.definition.ammunition_cost)
|
|
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:
|
|
amount = _effective_energy_cost(amount)
|
|
var current_energy := float(ship.get("energy"))
|
|
if current_energy < amount:
|
|
return false
|
|
ship.set("energy", current_energy - amount)
|
|
return true
|
|
|
|
func _effective_energy_cost(amount: float) -> float:
|
|
return float(ship.get_weapon_energy_cost(amount)) if ship.has_method("get_weapon_energy_cost") else amount
|
|
|
|
func _apply_damage(target: Variant, damage: float, critical: bool = false, piercing: float = 0.0, damage_type: int = DamageTypes.Type.ENERGY) -> void:
|
|
_last_hit_targets = {}
|
|
preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(target,damage,critical,damage_type,piercing,_last_hit_targets)
|
|
|
|
func _spawn_laser_impact(at_position: Vector2, kind := DamageTypes.Type.ENERGY, incoming := Vector2.UP) -> 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))
|
|
effect.call("configure_contact",kind,incoming)
|