Randoloot/scripts/game/enemy_ship.gd

219 lines
9.0 KiB
GDScript

extends CharacterBody2D
const DAMAGE_POPUP_SCENE := preload("res://scenes/game/damage_popup.tscn")
const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn")
const WEAPON_AUDIO_SCRIPT := preload("res://scripts/game/weapon_audio.gd")
const ITEM_GENERATOR := preload("res://scripts/items/item_generator.gd")
const LOOT_PICKUP := preload("res://scripts/items/loot_pickup.gd")
const LOOT_OFFSET := Vector2(0, 36)
const LEVEL_BALANCE := preload("res://scripts/levels/level_balance.gd")
@export_enum("Normal", "Elite", "Boss") var loot_tier: int = 0
@export var max_hull := 50.0
@export var activation_range := 760.0
@export var approach_speed := 62.0
@export var approach_acceleration := 85.0
@export var turn_speed := 1.8
@export var weapon_range := 680.0
@export var weapon_cooldown := 1.5
@export var weapon_damage := 8.0
var max_shields := 20.0
var shields := 20.0
var absorption := Vector3.ZERO
var weapon_kind := ItemDefinition.WeaponKind.PLASMA
var _shield_visual: Node2D
var hull := 50.0
var _player: CharacterBody2D
var _activated := false
var _sprite: Sprite2D
var _weapon_timer := 0.0
func _ready() -> void:
hull = max_hull
_sprite = $Sprite2D
_shield_visual = Node2D.new()
_shield_visual.set_script(preload("res://scripts/game/shield_visual.gd"))
_shield_visual.scale = Vector2.ONE * get_visual_size() / 64.0
add_child(_shield_visual)
_shield_visual.set_shield_values(shields, max_shields)
add_to_group("enemies")
_weapon_timer = randf_range(0.2, weapon_cooldown)
func apply_level_settings(level: int, tier: int) -> void:
loot_tier = tier
$Sprite2D.modulate = get_tier_tint()
var power := LEVEL_BALANCE.power_scale(level)
var combat_multiplier := 2.0 if tier == 2 else 1.25
var boss_multiplier := 2.0 if tier == 2 else 1.0
var hull_multiplier := 4.0 if tier == 2 else (2.0 if tier == 1 else 1.0)
var damage_bonus := 4.0 if tier == 2 else (1.3 if tier == 1 else 1.0)
max_hull = 50.0 * power * hull_multiplier * combat_multiplier
hull = max_hull
max_shields = max_hull * 0.4
shields = max_shields
var profile := DamageTypes.enemy_profile(level, GameState.system_seed)
weapon_kind = int(profile["kind"])
absorption = profile["absorption"]
weapon_damage = 8.0 * power * damage_bonus * combat_multiplier
approach_speed = 62.0 * (1.0 + 0.0005 * (clampi(level, 1, 1000) - 1)) * boss_multiplier
approach_acceleration = 85.0 * boss_multiplier
turn_speed = 1.8 * boss_multiplier
activation_range = 760.0 * boss_multiplier
weapon_range = 680.0 * boss_multiplier
weapon_cooldown = maxf(0.55, 1.5 / (1.0 + 0.001 * (clampi(level, 1, 1000) - 1))) / boss_multiplier
$CollisionShape2D.scale = Vector2.ONE * boss_multiplier
if is_instance_valid(_shield_visual):
_shield_visual.scale = Vector2.ONE * get_visual_size() / 64.0
_shield_visual.set_shield_values(shields, max_shields)
func configure(player: CharacterBody2D, ship_texture: Texture2D) -> void:
_player = player
_sprite = $Sprite2D
_sprite.texture = ship_texture
var largest_dimension := maxf(ship_texture.get_width(), ship_texture.get_height())
var visual_size := get_visual_size()
_sprite.scale = Vector2.ONE * (visual_size / largest_dimension)
_sprite.modulate = get_tier_tint()
preload("res://scripts/game/ship_shadow.gd").attach_to(_sprite)
func get_tier_tint() -> Color:
return Color("ffb3df") if loot_tier == 2 else (Color("ffc785") if loot_tier == 1 else Color.WHITE)
func get_visual_size() -> float:
return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0)
func _physics_process(delta: float) -> void:
if not is_instance_valid(_player):
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
move_and_slide()
return
var to_player := _player.global_position - global_position
if to_player.length() <= activation_range:
_activated = true
if _activated and to_player.length_squared() > 1.0:
var direction := to_player.normalized()
var desired_rotation := direction.angle() + PI * 0.5
rotation = rotate_toward(rotation, desired_rotation, turn_speed * delta)
velocity = velocity.move_toward(direction * approach_speed, approach_acceleration * delta)
_weapon_timer = maxf(0.0, _weapon_timer - delta)
if to_player.length() <= weapon_range and _weapon_timer <= 0.0:
_fire_plasma(direction)
_weapon_timer = weapon_cooldown
else:
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
move_and_slide()
_check_player_collision()
func _check_player_collision() -> void:
for collision_index in get_slide_collision_count():
var collision := get_slide_collision(collision_index)
if collision.get_collider() == _player and _player.has_method("register_enemy_collision"):
_player.call("register_enemy_collision", global_position)
func _fire_plasma(direction: Vector2) -> void:
var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0)
if weapon_kind == ItemDefinition.WeaponKind.LASER:
var endpoint := muzzle + direction * weapon_range
var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, 3, [get_rid()])
var hit := get_world_2d().direct_space_state.intersect_ray(query)
if not hit.is_empty():
endpoint = hit["position"]
var target: Node = hit.get("collider")
if target is CharacterBody2D and target.has_method("take_damage"):
target.take_damage(weapon_damage, false, DamageTypes.Type.ENERGY)
var beam := Line2D.new()
beam.set_script(preload("res://scripts/game/laser_beam_visual.gd"))
beam.width = 4.0
beam.default_color = Color(1.0, 0.3, 0.3)
beam.add_point(muzzle)
beam.add_point(endpoint)
get_parent().add_child(beam)
get_tree().create_timer(0.1).timeout.connect(beam.queue_free)
else:
var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate()
projectile.enemy_shot = true
projectile.collision_mask = 3
projectile.missile_homing = false
var speed := 430.0 if weapon_kind == ItemDefinition.WeaponKind.GAMMA else (950.0 if weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN else 600.0)
projectile.configure(muzzle, direction, weapon_kind, speed, weapon_damage, weapon_range)
get_parent().add_child(projectile)
WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, weapon_kind, -4.0)
func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTypes.Type.KINETIC) -> void:
if is_queued_for_deletion() or hull <= 0.0:
return
var before := hull + shields
take_damage(amount, critical, damage_type)
if is_instance_valid(_player):
_player.call("restore_shields_from_damage", maxf(0.0, before - hull - shields))
func take_damage(amount: float, critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC) -> void:
if is_queued_for_deletion() or hull <= 0.0:
return
var actual := maxf(0.0, amount) * (1.0 - absorption[damage_type] / 100.0)
var shield_damage := minf(shields, actual)
shields -= shield_damage
hull = maxf(0.0, hull - actual + shield_damage)
if is_instance_valid(_shield_visual):
_shield_visual.set_shield_values(shields, max_shields)
if shield_damage > 0.0:
_shield_visual.flash_hit()
_show_damage_popup(actual, critical)
if hull <= 0.0:
_reward_captain()
_drop_loot()
_spawn_destruction_effect()
queue_free()
func get_weapon_dps() -> float:
return weapon_damage / maxf(0.001, weapon_cooldown) * (1.0 + 0.25 * minf(3.0, weapon_cooldown) if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0)
func _reward_captain() -> void:
if is_instance_valid(_player):
var reward := GameState.captain.reward_enemy(GameState.system_level, loot_tier, float(_player.get("experience_multiplier")), float(_player.get("credits_multiplier")), false)
if reward.y > 0:
var credits := Node2D.new()
credits.set_script(preload("res://scripts/items/credit_pickup.gd"))
credits.set("amount", reward.y)
credits.position = position - LOOT_OFFSET
get_parent().call_deferred("add_child", credits)
func _drop_loot() -> void:
var chance := 0.65 if loot_tier == 0 else (0.9 if loot_tier == 1 else 1.0)
if randf() > chance:
return
var pickup := Node2D.new()
pickup.set_script(LOOT_PICKUP)
pickup.set("item", ITEM_GENERATOR.generate(GameState.system_level, loot_tier))
pickup.position = position + LOOT_OFFSET
get_parent().call_deferred("add_child", pickup)
func _spawn_destruction_effect() -> void:
var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D
effect.global_position = global_position
get_parent().add_child(effect)
effect.call("configure", true, Color(1.0, 0.36, 0.18))
func _show_damage_popup(amount: float, critical: bool = false) -> void:
if critical:
var flash := Node2D.new()
flash.set_script(preload("res://scripts/game/combat_flash.gd"))
flash.set("flash_color", Color("ffbc5c"))
flash.position = position
get_parent().add_child(flash)
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
popup.text = "%s-%s DMG" % ["KRYT! " if critical else "", _format_damage(amount)]
popup.set("is_critical", critical)
popup.global_position = global_position + Vector2(randf_range(-16.0, 16.0), -28.0)
get_parent().add_child(popup)
func _format_damage(amount: float) -> String:
if is_equal_approx(amount, roundf(amount)):
return str(int(roundf(amount)))
return "%.1f" % amount