304 lines
12 KiB
GDScript
304 lines
12 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")
|
|
|
|
@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 := 540.0
|
|
@export var space_drag := 24.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_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 damage_multiplier := 1.0
|
|
var experience_multiplier := 1.0
|
|
var credits_multiplier := 1.0
|
|
var critical_chance_bonus := 0.0
|
|
var critical_damage_bonus := 0.0
|
|
|
|
var angular_velocity := 0.0
|
|
var _sprite: Sprite2D
|
|
var _contact_damage_timer := 0.0
|
|
var _is_destroyed := false
|
|
var is_docked := false
|
|
var _boost_speed := 0.0
|
|
var _boost_target := 0.0
|
|
var _boost_acceleration := 0.0
|
|
var _boost_velocity_acceleration := 0.0
|
|
@onready var _shield_visual: Node2D = $ShieldVisual
|
|
|
|
func _enter_tree() -> void:
|
|
# 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:
|
|
max_hull = (base_max_hull + float(bonuses.get("max_hull", 0.0))) * (1.0 + float(bonuses.get("hull_percent", 0.0)) / 100.0)
|
|
max_shields = (base_max_shields + float(bonuses.get("max_shields", 0.0))) * (1.0 + float(bonuses.get("shield_percent", 0.0)) / 100.0)
|
|
max_energy = base_max_energy + float(bonuses.get("max_energy", 0.0))
|
|
max_speed = (base_max_speed + float(bonuses.get("max_speed", 0.0))) * (1.0 + float(bonuses.get("speed_percent", 0.0)) / 100.0)
|
|
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
|
|
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))
|
|
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:
|
|
for index in 6:
|
|
_ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key)
|
|
_ensure_input_action(&"ship_thrust", KEY_W)
|
|
_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)
|
|
|
|
_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)
|
|
# 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_damage(amount: float) -> void:
|
|
if _is_destroyed or is_docked or amount <= 0.0:
|
|
return
|
|
var hit := COMBAT_ROLLS.incoming_hit(amount, get_evasion_chance(), get_damage_reduction())
|
|
if hit["evaded"]:
|
|
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
|
|
popup.text = "UNIK"
|
|
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 shield_damage := minf(shields, remaining_damage)
|
|
shields -= shield_damage
|
|
remaining_damage -= shield_damage
|
|
hull = maxf(0.0, hull - remaining_damage)
|
|
if shield_damage > 0.0 and shields > 0.0:
|
|
_shield_visual.call("flash_hit")
|
|
_update_shield_visual()
|
|
if hull <= 0.0:
|
|
_destroy_ship()
|
|
|
|
func get_evasion_chance() -> float:
|
|
return GameState.captain.get_skill_rank("piloting") / 100.0
|
|
|
|
func get_damage_reduction() -> float:
|
|
return GameState.captain.get_skill_rank("mechanic") / 100.0
|
|
|
|
func get_weapon_damage_multiplier() -> float:
|
|
return damage_multiplier * (1.0 + GameState.captain.get_skill_rank("gunner") / 100.0)
|
|
|
|
func get_critical_chance() -> float:
|
|
return clampf(critical_chance_bonus + GameState.captain.get_skill_rank("sharp_eye") / 100.0, 0.0, 1.0)
|
|
|
|
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:
|
|
_is_destroyed = true
|
|
velocity = Vector2.ZERO
|
|
collision_layer = 0
|
|
collision_mask = 0
|
|
var tween := create_tween()
|
|
tween.tween_property(self, "modulate:a", 0.0, 1.1)
|
|
_restart_level_after_destruction()
|
|
|
|
func _restart_level_after_destruction() -> void:
|
|
await get_tree().create_timer(1.1).timeout
|
|
if is_inside_tree():
|
|
$ShipInventory.preserve_for_restart()
|
|
get_tree().reload_current_scene()
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if _is_destroyed or is_docked:
|
|
return
|
|
_contact_damage_timer = maxf(0.0, _contact_damage_timer - delta)
|
|
energy = minf(max_energy, energy + energy_regeneration * delta)
|
|
var was_boosting := is_boosting()
|
|
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:
|
|
var previous_shields := shields
|
|
shields = minf(max_shields, shields + shield_regeneration * delta)
|
|
if shields != previous_shields:
|
|
_update_shield_visual()
|
|
var turn_input := Input.get_axis("ship_turn_left", "ship_turn_right")
|
|
if not is_zero_approx(turn_input):
|
|
angular_velocity = move_toward(
|
|
angular_velocity,
|
|
turn_input * max_turn_speed,
|
|
turn_acceleration * delta
|
|
)
|
|
else:
|
|
angular_velocity = move_toward(angular_velocity, 0.0, turn_damping * delta)
|
|
rotation += angular_velocity * delta
|
|
|
|
var forward := Vector2.UP.rotated(rotation)
|
|
if Input.is_action_pressed("ship_brake"):
|
|
var forward_speed := velocity.dot(forward)
|
|
if forward_speed > 0.0:
|
|
velocity = velocity.move_toward(Vector2.ZERO, brake_acceleration * delta)
|
|
else:
|
|
velocity -= forward * thrust_acceleration * delta
|
|
var reverse_speed := velocity.dot(forward)
|
|
if reverse_speed < -reverse_max_speed:
|
|
velocity += forward * (-reverse_max_speed - reverse_speed)
|
|
elif Input.is_action_pressed("ship_thrust"):
|
|
velocity += forward * thrust_acceleration * delta
|
|
var forward_speed := velocity.dot(forward)
|
|
if forward_speed > max_speed and not was_boosting:
|
|
velocity += forward * (max_speed - forward_speed)
|
|
|
|
var strafe_input := Input.get_axis("ship_strafe_left", "ship_strafe_right")
|
|
var right := Vector2.RIGHT.rotated(rotation)
|
|
if not is_zero_approx(strafe_input):
|
|
velocity += right * strafe_input * thrust_acceleration * delta
|
|
var strafe_speed := velocity.dot(right)
|
|
velocity += right * (clampf(strafe_speed, -max_strafe_speed, max_strafe_speed) - strafe_speed)
|
|
|
|
if (
|
|
not was_boosting
|
|
and
|
|
not Input.is_action_pressed("ship_brake")
|
|
and not Input.is_action_pressed("ship_thrust")
|
|
and is_zero_approx(strafe_input)
|
|
):
|
|
velocity = velocity.move_toward(Vector2.ZERO, space_drag * delta)
|
|
|
|
_clamp_velocity_to_max_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 := max_speed + _boost_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
|
|
|
|
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("module", slot_index)
|
|
return item.definition.active_ability if item != null and item.definition != null else null
|
|
|
|
func ability_unavailable_reason(slot_index: int) -> String:
|
|
if _is_destroyed or is_docked:
|
|
return "Statek niedostępny"
|
|
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
|
|
get_active_ability(slot_index).activate(self)
|
|
return true
|
|
|
|
func _unhandled_key_input(event: InputEvent) -> void:
|
|
if event is InputEventKey and event.echo:
|
|
return
|
|
for index in 6:
|
|
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)
|