Randoloot/scripts/game/ship_controller.gd
2026-10-25 22:38:13 +01:00

635 lines
28 KiB
GDScript

extends CharacterBody2D
const DEFAULT_SHIP_TEXTURE := "res://assets/Ships/Spaceship_0.png"
const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn")
const DAMAGE_POPUP_SCENE := preload("res://scenes/game/damage_popup.tscn")
const COMBAT_ROLLS := preload("res://scripts/game/combat_rolls.gd")
const CAPTAIN_BALANCE := preload("res://scripts/captain/captain_balance.gd")
const SHIP_CATALOG := preload("res://scripts/game/ship_catalog.gd")
const COMBAT_FLASH := preload("res://scripts/game/combat_flash.gd")
const MINI_BOOST_ENERGY_PER_SECOND := 20.0
const EQUIPMENT_BALANCE := preload("res://scripts/items/equipment_balance.gd")
const DESTRUCTION_EFFECT := preload("res://scripts/game/player_destruction_effect.gd")
@export var max_speed := 76.0
@export var reverse_max_speed := 210.0
@export var max_strafe_speed := 210.0
@export var thrust_acceleration := 90.0
@export var brake_acceleration := 180.0
@export var space_drag := 16.0
@export var max_turn_speed := 2.8
@export var turn_acceleration := 7.0
@export var turn_damping := 4.0
@export var base_max_hull := 100.0
@export var base_max_shields := 100.0
@export var base_scanner_range := 1000.0
var scanner_range := 1000.0
@export var base_max_energy := 100.0
@export var base_max_speed := 90.0
@export var base_energy_regeneration := 5.0
@export var base_shield_regeneration := 0.0
@export var max_hull := 100.0
@export var max_shields := 100.0
@export var max_energy := 100.0
var hull := 100.0
var shields := 100.0
var energy := 100.0
var energy_regeneration := 5.0
var shield_regeneration := 0.0
var absorption := Vector3.ZERO
var damage_multiplier := 1.0
var experience_multiplier := 1.0
var credits_multiplier := 1.0
var luck_percent := 0.0
var shield_on_damage_percent := 0.0
var critical_chance_bonus := 0.0
var critical_damage_bonus := 0.0
@export var solar_charge_radius := 550.0
@export var solar_charge_full_radius := 300.0
@export var solar_charge_fraction_per_second := 0.08
var charging_star: Node2D
var angular_velocity := 0.0
var _sprite: Sprite2D
var _contact_damage_timer := 0.0
var _is_destroyed := false
var _restart_requested := false
var is_docked := false
var landing_combat_lock := 0.0
var _boost_speed := 0.0
var _boost_target := 0.0
var _boost_acceleration := 0.0
var _boost_velocity_acceleration := 0.0
var _mini_boost_active := false
var _mini_boost_exhausted := false
var _mini_boost_coast_speed := 0.0
var _shield_leech_budget := -1.0
var _polarization_budget := -1.0
var _reactive_armor_cooldown := 0.0
var _shield_reserve_cooldown := 0.0
var _shield_reserve_remaining := 0.0
var _salvo_cooldown := 0.0
var _emergency_shield_cooldown := 0.0
var _emergency_shield_remaining := 0.0
var _bulkhead_cooldown := 0.0
var _bulkhead_repair_remaining := 0.0
var _bastion_invulnerability := 0.0
@onready var _shield_visual: Node2D = $ShieldVisual
func _enter_tree() -> void:
GameState.captain.reset_combat_state()
# Set the chassis before inventory children calculate equipment bonuses.
var chassis := SHIP_CATALOG.for_path(GameState.selected_ship_path)
base_max_hull = float(chassis["hull"])
base_max_shields = float(chassis["shields"])
base_max_energy = float(chassis["energy"])
base_max_speed = float(chassis["speed"])
func apply_equipment_bonuses(bonuses: Dictionary) -> void:
bonuses = EQUIPMENT_BALANCE.bonuses(bonuses)
absorption = DamageTypes.capped(Vector3(float(bonuses.get("kinetic_absorption", 0.0)), float(bonuses.get("energy_absorption", 0.0)), float(bonuses.get("radiation_absorption", 0.0))))
max_hull = (base_max_hull + float(bonuses.get("max_hull", 0.0))) * (1.0 + float(bonuses.get("hull_percent", 0.0)) / 100.0)
if preload("res://scripts/planets/alien_research.gd").learned("hull_b"): max_hull *= 1.1
max_shields = (base_max_shields + float(bonuses.get("max_shields", 0.0))) * (1.0 + float(bonuses.get("shield_percent", 0.0)) / 100.0)
if preload("res://scripts/planets/alien_research.gd").learned("shields_b"): max_shields *= 1.1
scanner_range = base_scanner_range + float(bonuses.get("scanner_range", 0.0))
max_energy = base_max_energy + float(bonuses.get("max_energy", 0.0))
max_speed = EQUIPMENT_BALANCE.speed(base_max_speed,float(bonuses.get("max_speed",0.0)),float(bonuses.get("speed_percent",0.0)))
space_drag = maxf(16.0,max_speed*0.2)
damage_multiplier = 1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0
experience_multiplier = 1.0 + float(bonuses.get("experience_percent", 0.0)) / 100.0
credits_multiplier = 1.0 + float(bonuses.get("credits_percent", 0.0)) / 100.0
luck_percent = maxf(0.0, float(bonuses.get("luck_percent", 0.0)))
shield_on_damage_percent = maxf(0.0, float(bonuses.get("shield_on_damage_percent", 0.0)))
critical_chance_bonus = float(bonuses.get("critical_chance_percent", 0.0)) / 100.0
critical_damage_bonus = float(bonuses.get("critical_damage_percent", 0.0)) / 100.0
energy_regeneration = base_energy_regeneration + float(bonuses.get("energy_regeneration", 0.0))
shield_regeneration = base_shield_regeneration + float(bonuses.get("shield_regeneration", 0.0))
energy_regeneration = EQUIPMENT_BALANCE.energy_regen(energy_regeneration,max_energy)
shield_regeneration = EQUIPMENT_BALANCE.shield_regen(shield_regeneration,max_shields)
var leech_capacity := max_shields*EQUIPMENT_BALANCE.SHIELD_LEECH_FRACTION
var polarization_capacity := max_energy*EQUIPMENT_BALANCE.POLARIZATION_FRACTION
_shield_leech_budget = leech_capacity if _shield_leech_budget < 0 else minf(_shield_leech_budget,leech_capacity)
_polarization_budget = polarization_capacity if _polarization_budget < 0 else minf(_polarization_budget,polarization_capacity)
reverse_max_speed = max_speed * 0.5
max_strafe_speed = max_speed * 0.5
hull = minf(hull, max_hull)
shields = minf(shields, max_shields)
energy = minf(energy, max_energy)
_update_shield_visual()
func _ready() -> void:
var extension_systems := Node.new()
extension_systems.name = "ExtensionSystems"
extension_systems.set_script(preload("res://scripts/game/extension_systems.gd"))
extension_systems.set("cooldowns", $ShipInventory.extension_cooldowns)
add_child(extension_systems)
for index in ShipInventory.EXTENSION_SLOT_COUNT:
_ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key)
_ensure_input_action(&"use_energy_bank", KEY_R)
_ensure_input_action(&"ship_thrust", KEY_W)
_ensure_input_action(&"ship_mini_boost", KEY_TAB)
_ensure_input_action(&"ship_brake", KEY_S)
_ensure_input_action(&"ship_turn_left", KEY_A)
_ensure_input_action(&"ship_turn_right", KEY_D)
_ensure_input_action(&"ship_strafe_left", KEY_Q)
_ensure_input_action(&"ship_strafe_right", KEY_E)
var scanner_layer := CanvasLayer.new()
scanner_layer.name = "ScannerOverlay"
scanner_layer.layer = preload("res://scripts/ui/render_layers.gd").SCANNER
add_child(scanner_layer)
var scanner := Control.new()
scanner.set_script(preload("res://scripts/ui/scanner_overlay.gd"))
scanner.set("player", self)
scanner_layer.add_child(scanner)
scanner.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_sprite = $Sprite2D
var texture_path := SHIP_CATALOG.texture_path(SHIP_CATALOG.for_path(GameState.selected_ship_path))
if texture_path.is_empty() or not ResourceLoader.exists(texture_path):
texture_path = DEFAULT_SHIP_TEXTURE
var ship_texture := load(texture_path) as Texture2D
if ship_texture:
_sprite.texture = ship_texture
var largest_dimension := maxf(ship_texture.get_width(), ship_texture.get_height())
_sprite.scale = Vector2.ONE * (64.0 / largest_dimension)
preload("res://scripts/game/ship_shadow.gd").attach_to(_sprite)
# Inventory children have already applied the restored equipment bonuses.
hull = max_hull
shields = max_shields
energy = max_energy
_update_shield_visual()
func _ensure_input_action(action_name: StringName, physical_key: Key) -> void:
if not InputMap.has_action(action_name):
InputMap.add_action(action_name, 0.5)
var event := InputEventKey.new()
event.physical_keycode = physical_key
if not InputMap.action_has_event(action_name, event):
InputMap.action_add_event(action_name, event)
func take_enemy_weapon_hit(amount: float, critical: bool, damage_type: int, source: Node) -> void:
if is_instance_valid(source) and source is Node2D and not has_meta(preload("res://scripts/game/combat_vfx.gd").CONTEXT):
var incoming: Vector2 = (global_position-source.global_position).normalized()
preload("res://scripts/game/combat_vfx.gd").mark_hit(self,global_position-incoming*28,incoming)
take_damage(amount,critical,damage_type,source)
func get_weapon_energy_cost(amount: float) -> float:
return amount*0.5 if _shield_reserve_remaining > 0.0 else amount
func take_damage(amount: float, _critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC, source: Node = null) -> void:
var visual_contact := preload("res://scripts/game/combat_vfx.gd").consume(self)
if amount > 0 and not is_docked:
landing_combat_lock = 6.0
if _is_destroyed or is_docked or amount <= 0.0:
return
if _bastion_invulnerability > 0.0: return
var hit := COMBAT_ROLLS.incoming_hit(amount * (1.0 - absorption[damage_type] / 100.0), get_evasion_chance(), get_damage_reduction())
if hit["evaded"]:
var flash := Node2D.new()
flash.set_script(COMBAT_FLASH)
flash.set("flash_color", Color("77e8bc"))
flash.position = position
get_parent().add_child(flash)
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
popup.text = "UNIK"
popup.set("is_evade", true)
popup.set("text_color", Color("77e8bc"))
popup.position = position + Vector2(-20, -38)
get_parent().add_child(popup)
return
var remaining_damage: float = hit["damage"]
var research := preload("res://scripts/planets/alien_research.gd")
var shields_before := shields
if shields > 0.0:
if research.learned("shields_b_next_next") and _salvo_cooldown <= 0.0:
remaining_damage *= 0.1
_salvo_cooldown = 60.0
_hull_technology_feedback("ROZPROSZENIE SALWY")
elif _emergency_shield_remaining > 0.0:
remaining_damage *= 0.5
var shield_damage := minf(shields, remaining_damage)
shields -= shield_damage
if shield_damage > 0.0:
var shield: ItemInstance = $ShipInventory.get_item("module",0)
if shield != null and "polarization" in shield.special_orbs:
var recovered := minf(max_shields*0.05,minf(shield_damage,maxf(0.0,_polarization_budget)))
recovered = minf(recovered,maxf(0.0,max_energy-energy))
energy += recovered
_polarization_budget -= recovered
remaining_damage -= shield_damage
if shields_before >= max_shields*0.3 and shields < max_shields*0.3 and research.learned("shields_b_next") and _shield_reserve_cooldown <= 0.0:
_shield_reserve_cooldown = 15.0
_shield_reserve_remaining = 2.0
_hull_technology_feedback("REZERWA KONDENSATORÓW")
if shields_before > 0.0 and shields <= 0.0 and research.learned("shields_b_next_next_next") and _emergency_shield_cooldown <= 0.0 and not GameState.captain.is_skill_active("power_redirect"):
shields = max_shields*0.25
_emergency_shield_cooldown = 30.0
_emergency_shield_remaining = 2.0
_hull_technology_feedback("TARCZA AWARYJNA")
if shield_damage > 0.0 and research.learned("shields_a_next_next_next") and is_instance_valid(source) and source.is_in_group("enemies") and not bool(source.get("virus_controlled")) and float(source.get("hull")) > 0.0:
var effect := Node2D.new()
effect.set_script(preload("res://scripts/game/shield_research_effect.gd"))
effect.position = global_position
effect.endpoint = source.global_position-global_position
get_parent().add_child(effect)
source.take_damage(shield_damage*0.2,false,DamageTypes.Type.ENERGY)
if remaining_damage > 0.0 and research.learned("hull_b_next") and _reactive_armor_cooldown <= 0.0:
remaining_damage *= 0.3
_reactive_armor_cooldown = 15.0
_hull_technology_feedback("PANCERZ REAKTYWNY")
var hull_before := hull
hull = maxf(0.0, hull - remaining_damage)
if hull <= 0.0 and research.learned("hull_b_next_next_next") and not GameState.hull_bastion_used:
hull = 1.0
GameState.hull_bastion_used = true
_bastion_invulnerability = 2.0
_hull_technology_feedback("OSTATNI BASTION")
if hull > 0.0 and hull_before >= max_hull*0.3 and hull < max_hull*0.3 and research.learned("hull_b_next_next") and _bulkhead_cooldown <= 0.0:
_bulkhead_cooldown = 45.0
_bulkhead_repair_remaining = 5.0
_hull_technology_feedback("AWARYJNE GRODZIE")
preload("res://scripts/game/combat_vfx.gd").feedback(self,shield_damage,maxf(0,hull_before-hull),damage_type,visual_contact)
_update_shield_visual()
if hull <= 0.0:
_destroy_ship()
func tick_hull_technologies(delta: float) -> void:
if _is_destroyed or is_docked: return
_shield_reserve_cooldown = maxf(0.0,_shield_reserve_cooldown-delta)
_shield_reserve_remaining = maxf(0.0,_shield_reserve_remaining-delta)
_salvo_cooldown = maxf(0.0,_salvo_cooldown-delta)
_emergency_shield_cooldown = maxf(0.0,_emergency_shield_cooldown-delta)
_emergency_shield_remaining = maxf(0.0,_emergency_shield_remaining-delta)
_reactive_armor_cooldown = maxf(0.0,_reactive_armor_cooldown-delta)
_bulkhead_cooldown = maxf(0.0,_bulkhead_cooldown-delta)
_bastion_invulnerability = maxf(0.0,_bastion_invulnerability-delta)
if _bulkhead_repair_remaining > 0.0:
var step := minf(delta,_bulkhead_repair_remaining)
_bulkhead_repair_remaining = maxf(0.0,_bulkhead_repair_remaining-delta)
hull = minf(max_hull,hull+max_hull*0.03*step)
func _hull_technology_feedback(text: String) -> void:
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
popup.text = text
popup.set("text_color",Color("79efb2"))
popup.position = position+Vector2(-60,-48)
get_parent().add_child(popup)
var flash := Node2D.new()
flash.set_script(COMBAT_FLASH)
flash.set("flash_color",Color("79efb2"))
flash.position = position
get_parent().add_child(flash)
func restore_shields_from_damage(actual_damage: float) -> void:
if _is_destroyed or actual_damage <= 0.0 or shield_on_damage_percent <= 0.0 or GameState.captain.is_skill_active("power_redirect"):
return
var recovered := minf(actual_damage*minf(10.0,shield_on_damage_percent)/100.0,maxf(0.0,_shield_leech_budget))
recovered = minf(recovered,maxf(0.0,max_shields-shields))
shields += recovered
_shield_leech_budget -= recovered
_update_shield_visual()
func get_passive_extension_value(effect: String) -> float:
var best := 0.0
for item in $ShipInventory.extension_slots.slice(0,$ShipInventory.unlocked_extension_slots):
if item != null and item.is_passive_extension() and item.definition.passive_effect == effect:
best = maxf(best, item.get_passive_value())
return best
func recycle_asteroid_fragment() -> void:
if _is_destroyed:
return
var fraction := get_passive_extension_value("recycler") / 100.0
if fraction <= 0.0:
return
energy = minf(max_energy, energy + max_energy * fraction)
if not GameState.captain.is_skill_active("power_redirect"):
shields = minf(max_shields, shields + max_shields * fraction)
_update_shield_visual()
func try_control_enemy(enemy: CharacterBody2D) -> bool:
if not is_instance_valid(enemy) or int(enemy.get("loot_tier")) == 2:
return false
var limit := int(get_passive_extension_value("virus"))
if limit <= 0 or _is_destroyed:
return false
var controlled := 0
for ally in get_tree().get_nodes_in_group("virus_allies"):
if not ally.is_queued_for_deletion() and float(ally.get("hull")) > 0.0:
controlled += 1
if controlled >= limit:
return false
if not bool(enemy.call("roll_virus_infection")):
return false
enemy.call("set_virus_controlled", true)
return true
func _sync_virus_allies() -> void:
var remaining := int(get_passive_extension_value("virus")) if not _is_destroyed else 0
for ally in get_tree().get_nodes_in_group("virus_allies"):
if ally.is_queued_for_deletion():
continue
if remaining > 0:
remaining -= 1
else:
ally.call("set_virus_controlled", false)
func get_evasion_chance() -> float:
var base := GameState.captain.get_skill_bonus("tactical_maneuver") if GameState.captain.is_skill_active("tactical_maneuver") else 0.0
return 1.0-(1.0-base)*0.95 if preload("res://scripts/planets/alien_research.gd").learned("weapons_b") else base
func get_energy_regeneration() -> float:
return (energy_regeneration + get_solar_regeneration()) * GameState.captain.energy_regeneration_multiplier()
func get_damage_reduction() -> float:
var reduction := GameState.captain.get_skill_bonus("mechanic")
var threshold := get_passive_extension_value("titanium_armor") / 100.0
if threshold > 0.0 and hull / maxf(1.0, max_hull) <= threshold:
reduction = 1.0 - (1.0 - reduction) * 0.5
return reduction
func get_weapon_damage_multiplier() -> float:
return damage_multiplier * (1.0 + GameState.captain.get_skill_bonus("gunner"))
func get_critical_chance() -> float:
return clampf(critical_chance_bonus + GameState.captain.get_skill_bonus("sharp_eye"), 0.0, 0.60)
func get_critical_multiplier() -> float:
return CAPTAIN_BALANCE.BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, critical_damage_bonus))
func roll_weapon_hit(item: ItemInstance) -> Dictionary:
return COMBAT_ROLLS.weapon_hit(item.roll_damage(), get_weapon_damage_multiplier(), get_critical_chance(), get_critical_multiplier())
func register_enemy_collision(enemy_position: Vector2) -> void:
if _is_destroyed or _contact_damage_timer > 0.0:
return
var push_direction := global_position - enemy_position
if push_direction.length_squared() < 0.001:
push_direction = -velocity if velocity.length_squared() > 0.001 else Vector2.UP.rotated(rotation)
push_direction = push_direction.normalized()
velocity = velocity.bounce(push_direction) * 0.55 + push_direction * 110.0
_clamp_velocity_to_max_speed()
_contact_damage_timer = 0.8
_spawn_hit_effect(global_position, Color(0.65, 0.9, 1.0))
take_damage(max_hull * 0.01)
func _spawn_hit_effect(at_position: Vector2, effect_color: Color) -> void:
var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D
effect.global_position = at_position
get_parent().add_child(effect)
effect.call("configure", false, effect_color)
func _update_shield_visual() -> void:
if is_instance_valid(_shield_visual):
_shield_visual.call("set_shield_values", shields, max_shields)
func _destroy_ship() -> void:
if _is_destroyed:
return
_is_destroyed = true
$ExtensionSystems.reset_combat_state()
GameState.captain.reset_combat_state()
var death_velocity := velocity
velocity = Vector2.ZERO
collision_layer = 0
collision_mask = 0
_mini_boost_active = false
var explosion := Node2D.new()
explosion.name = "PlayerDestructionEffect"
explosion.set_script(DESTRUCTION_EFFECT)
explosion.call("configure", _sprite, death_velocity, get_node_or_null("Camera2D"))
get_parent().add_child(explosion)
explosion.connect("finished", _show_defeat_screen)
_sprite.hide()
for node_name in ["ShieldVisual", "EngineVisual", "EnergyIndicator", "SolarEnergyEffect"]:
var visual := get_node_or_null(node_name) as CanvasItem
if visual != null:
visual.hide()
$EngineAudio.stop()
$EngineAudio.set_process(false)
$EngineVisual.set_process(false)
$ShieldVisual.set_process(false)
var radiation := get_node_or_null("RadiationStatus")
if radiation != null:
radiation.set_physics_process(false)
func _show_defeat_screen() -> void:
if not is_inside_tree() or get_parent().has_node("DefeatOverlay"):
return
var overlay := CanvasLayer.new()
overlay.set_script(preload("res://scripts/ui/defeat_overlay.gd"))
overlay.set("ship", self)
get_parent().add_child(overlay)
func _restart_level_after_destruction() -> void:
if not is_inside_tree() or _restart_requested: return
_restart_requested = true
_reload_after_destruction.call_deferred()
func _reload_after_destruction() -> void:
var bastion_used := GameState.hull_bastion_used
var active: Dictionary = GameState.active_distant_map.duplicate(true)
var home: Dictionary = GameState.distant_home.duplicate(true)
var level := GameState.system_level
var seed_value := GameState.system_seed
$ShipInventory.preserve_for_restart()
if not active.is_empty():
GameState.restore_distant_home()
GameState.return_to_freighter = true
else:
GameState.reset_current_system_exploration()
var error := get_tree().reload_current_scene()
if error != OK:
GameState.hull_bastion_used = bastion_used
GameState.active_distant_map = active
GameState.distant_home = home
GameState.system_level = level
GameState.system_seed = seed_value
GameState.return_to_freighter = false
GameState.restart_inventory.clear()
_restart_requested = false
var overlay := get_parent().get_node_or_null("DefeatOverlay")
if overlay != null: overlay.set("_retrying",false)
push_error("Could not recover after destruction: %s" % error_string(error))
func _physics_process(delta: float) -> void:
landing_combat_lock = maxf(0, landing_combat_lock - delta)
_sync_virus_allies()
if not _is_destroyed:
GameState.captain.tick_skills(delta)
if GameState.captain.is_skill_active("power_redirect"):
shields = 0.0
if _is_destroyed or is_docked:
_mini_boost_active = false
return
_refill_recovery_budgets(delta)
tick_hull_technologies(delta)
_contact_damage_timer = maxf(0.0, _contact_damage_timer - delta)
energy = minf(max_energy, energy + get_energy_regeneration() * delta)
var was_mini_boosting := _mini_boost_active
_update_mini_boost(delta)
if _mini_boost_active:
_mini_boost_coast_speed = 0.0
else:
if was_mini_boosting:
_mini_boost_coast_speed = maxf(0.0, velocity.length() - max_speed)
_mini_boost_coast_speed = move_toward(_mini_boost_coast_speed, 0.0, space_drag * delta)
var was_boosting := _boost_target > 0.0 or _boost_speed > 0.0
if _boost_target > 0.0:
_boost_speed = move_toward(_boost_speed, _boost_target, _boost_acceleration * delta)
velocity = velocity.move_toward(Vector2.UP.rotated(rotation) * (max_speed + _boost_target), _boost_velocity_acceleration * delta)
if _boost_speed >= _boost_target:
_boost_target = 0.0
else:
_boost_speed = move_toward(_boost_speed, 0.0, space_drag * delta)
if was_boosting:
velocity = velocity.move_toward(Vector2.ZERO, space_drag * delta)
if shield_regeneration > 0.0 and shields < max_shields and not GameState.captain.is_skill_active("power_redirect"):
var previous_shields := shields
shields = minf(max_shields, shields + shield_regeneration * delta)
if shields != previous_shields:
_update_shield_visual()
preload("res://scripts/game/flight_movement.gd").apply(
self, delta,
Input.get_axis("ship_turn_left", "ship_turn_right"),
Input.is_action_pressed("ship_thrust"),
Input.is_action_pressed("ship_brake"),
Input.get_axis("ship_strafe_left", "ship_strafe_right"),
_mini_boost_active, was_boosting, _get_allowed_speed())
move_and_slide()
_check_enemy_collisions()
_clamp_velocity_to_max_speed()
func _clamp_velocity_to_max_speed() -> void:
var speed_squared := velocity.length_squared()
var allowed_speed := _get_allowed_speed()
var max_speed_squared := allowed_speed * allowed_speed
if speed_squared > max_speed_squared:
velocity = velocity.normalized() * allowed_speed
func is_boosting() -> bool:
return _boost_target > 0.0 or _boost_speed > 0.0 or _mini_boost_active
func is_mini_boosting() -> bool:
return _mini_boost_active and not _is_destroyed and not is_docked
func _get_allowed_speed() -> float:
return minf(EQUIPMENT_BALANCE.MAX_BOOST_SPEED,maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed))
func has_free_mini_boost() -> bool:
var engine: ItemInstance = $ShipInventory.get_item("module",2)
return engine != null and engine.definition.category == ItemDefinition.Category.ENGINE and "booster" in engine.special_orbs
func _update_mini_boost(delta: float) -> void:
_mini_boost_active = false
if not Input.is_action_pressed("ship_mini_boost"):
_mini_boost_exhausted = false
return
if has_free_mini_boost() and not Input.is_action_pressed("ship_brake"):
_mini_boost_exhausted = false
_mini_boost_active = true
return
if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"):
return
var cost := get_mini_boost_energy_cost() * delta
if energy < cost or energy <= 0.0:
_mini_boost_exhausted = true
return
energy = maxf(0.0, energy - cost)
_mini_boost_active = true
func get_mini_boost_energy_cost() -> float:
if has_free_mini_boost(): return 0.0
var engine: ItemInstance = $ShipInventory.get_item("module",2)
var base_cost := maxf(MINI_BOOST_ENERGY_PER_SECOND,max_energy*0.22)
return base_cost * (0.5 if engine != null and "atomic_engine" in engine.special_orbs else 1.0)
func _refill_recovery_budgets(delta: float) -> void:
var leech := max_shields*EQUIPMENT_BALANCE.SHIELD_LEECH_FRACTION
var polarization := max_energy*EQUIPMENT_BALANCE.POLARIZATION_FRACTION
_shield_leech_budget = minf(leech,maxf(0.0,_shield_leech_budget)+leech*delta)
_polarization_budget = minf(polarization,maxf(0.0,_polarization_budget)+polarization*delta)
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.is_action("ship_mini_boost"):
# Tab controls the engine rather than moving focus between HUD buttons.
get_viewport().set_input_as_handled()
func get_solar_regeneration() -> float:
if _is_destroyed or is_docked or not is_instance_valid(charging_star):
return 0.0
var distance := global_position.distance_to(charging_star.global_position)
var strength := 1.0 - smoothstep(solar_charge_full_radius, solar_charge_radius, distance)
return max_energy * minf(solar_charge_fraction_per_second,0.08) * strength
func start_boost(speed_bonus: float, acceleration_time: float) -> void:
_boost_target = max_speed * maxf(0.0, speed_bonus)
_boost_acceleration = _boost_target / maxf(0.01, acceleration_time)
_boost_velocity_acceleration = (max_speed + _boost_target) / maxf(0.01, acceleration_time)
func get_active_ability(slot_index: int) -> ActiveAbility:
var item: ItemInstance = $ShipInventory.get_item("extension", slot_index)
return item.get_active_ability() if item != null and item.definition != null else null
func get_luck_chance() -> float:
return GameState.captain.get_luck_chance(luck_percent)
func is_cloaked() -> bool:
var systems := get_node_or_null("ExtensionSystems")
return systems != null and float(systems.get("cloak_remaining")) > 0.0 and not _is_destroyed
func get_ability_cooldown(slot_index: int) -> float:
var ability := get_active_ability(slot_index)
if ability == null or ability.ability_id.is_empty():
return 0.0
return float($ExtensionSystems.cooldowns.get(ability.ability_id, 0.0))
func ability_unavailable_reason(slot_index: int) -> String:
if _is_destroyed or is_docked or get_tree().paused:
return "Statek niedostępny"
var remaining := get_ability_cooldown(slot_index)
if remaining > 0.0:
return "Odnowienie: %d s" % ceili(remaining)
var ability := get_active_ability(slot_index)
return ability.unavailable_reason(self) if ability != null else "Brak rozszerzenia"
func activate_ability(slot_index: int) -> bool:
if not ability_unavailable_reason(slot_index).is_empty():
return false
var ability := get_active_ability(slot_index)
ability.activate(self)
if ability.cooldown > 0.0 and not ability.ability_id.is_empty():
$ExtensionSystems.cooldowns[ability.ability_id] = ability.cooldown
return true
func use_energy_bank() -> void:
if $ShipInventory.use_energy_bank():
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
popup.text = "+%d%% ENERGII" % roundi(GameState.captain.get_energy_bank_fraction() * 100.0)
popup.set("text_color", Color("70f5df"))
popup.position = position + Vector2(-48, -60)
get_parent().add_child(popup)
func _unhandled_key_input(event: InputEvent) -> void:
if event.is_action_pressed("use_energy_bank") and not event.is_echo():
use_energy_bank()
get_viewport().set_input_as_handled()
return
if event is InputEventKey and event.echo:
return
for index in ShipInventory.EXTENSION_SLOT_COUNT:
if event.is_action_pressed("ability_%d" % (index + 1)):
activate_ability(index)
get_viewport().set_input_as_handled()
return
func _check_enemy_collisions() -> void:
for collision_index in get_slide_collision_count():
var collision := get_slide_collision(collision_index)
var collider := collision.get_collider()
if collider is Node and collider.is_in_group("enemies"):
register_enemy_collision(collider.global_position)