572 lines
25 KiB
GDScript
572 lines
25 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")
|
|
const LASER_COOLDOWN_MULTIPLIER := 2.0
|
|
const LASER_MINIMUM_COOLDOWN := 1.4
|
|
const LASER_MISS_CHANCE := 0.45
|
|
const LASER_DAMAGE_MULTIPLIER := 0.8
|
|
|
|
@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
|
|
enum Tactic { RAM, KEEP_DISTANCE }
|
|
@export_enum("Losowa", "Taran", "Dystans") var tactic_choice := 0
|
|
var tactic := Tactic.RAM
|
|
var _orbit_direction := 1.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 _retreat_mine_rolled := false
|
|
var _sprite: Sprite2D
|
|
var _weapon_timer := 0.0
|
|
var _slow_remaining := 0.0
|
|
var _combat_target: Node2D
|
|
var virus_controlled := false
|
|
var _virus_infection_attempted := false
|
|
var _research_player_hit := false
|
|
var _research_direct_hit := false
|
|
var _weapon_piercing := 0.0
|
|
var combat_level := 1
|
|
var _reward_claimed := false
|
|
var _disabled_systems := [0.0,0.0,0.0]
|
|
var _offline_shields := 0.0
|
|
var _hull_breach_remaining := 0.0
|
|
var _execution_finish := false
|
|
var _shield_regen_lock := 0.0
|
|
var _shield_overload_cooldown := 0.0
|
|
var _research_damage_scale := 1.0
|
|
|
|
func roll_virus_infection() -> bool:
|
|
if loot_tier == 2 or _virus_infection_attempted:
|
|
return false
|
|
_virus_infection_attempted = true
|
|
return randf() < 0.5
|
|
|
|
func set_virus_controlled(value: bool) -> void:
|
|
if value and loot_tier == 2:
|
|
return
|
|
if virus_controlled == value or is_queued_for_deletion():
|
|
return
|
|
virus_controlled = value
|
|
_combat_target = null
|
|
_activated = true
|
|
if value:
|
|
remove_from_group("enemies")
|
|
add_to_group("virus_allies")
|
|
set_deferred("collision_layer", 64)
|
|
set_deferred("collision_mask", 5)
|
|
if not _reward_claimed:
|
|
_reward_claimed = true
|
|
_reward_captain()
|
|
_drop_loot()
|
|
else:
|
|
remove_from_group("virus_allies")
|
|
add_to_group("enemies")
|
|
set_deferred("collision_layer", 4)
|
|
set_deferred("collision_mask", 2)
|
|
if is_instance_valid(_sprite):
|
|
_sprite.modulate = Color("b986ff") if value else get_tier_tint()
|
|
queue_redraw()
|
|
|
|
func _draw() -> void:
|
|
if _hull_breach_remaining > 0.0:
|
|
for arc in 4:
|
|
var angle := arc*PI*0.5+0.12
|
|
draw_arc(Vector2.ZERO,get_visual_size()*0.55,angle,angle+0.9,12,Color(1.0,0.5,0.12,minf(0.9,_hull_breach_remaining)),1.8,true)
|
|
if virus_controlled:
|
|
draw_arc(Vector2.ZERO, get_visual_size() * 0.6, 0.0, TAU, 48, Color("c57dff"), 2.0, true)
|
|
for index in 3:
|
|
var marker := Vector2.from_angle(index * TAU / 3.0) * get_visual_size() * 0.67
|
|
draw_circle(marker, 2.5, Color("e0baff"))
|
|
|
|
|
|
func _ready() -> void:
|
|
tactic = randi_range(0, 1) if tactic_choice == 0 else tactic_choice - 1
|
|
_orbit_direction = -1.0 if randf() < 0.5 else 1.0
|
|
hull = max_hull
|
|
_sprite = $Sprite2D
|
|
_shield_visual = Node2D.new()
|
|
_shield_visual.name = "ShieldVisual"
|
|
_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, get_fire_interval())
|
|
|
|
func apply_level_settings(level: int, tier: int) -> void:
|
|
loot_tier = tier
|
|
combat_level = maxi(1, level)
|
|
$Sprite2D.modulate = get_tier_tint()
|
|
var power := LEVEL_BALANCE.enemy_stat_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, LEVEL_BALANCE.MAX_LEVEL) - 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 * (0.5 if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0)
|
|
weapon_cooldown = maxf(0.55, 1.5 / (1.0 + 0.001 * (clampi(level, 1, LEVEL_BALANCE.MAX_LEVEL) - 1))) / boss_multiplier
|
|
if tier == 2:
|
|
# Ease the first encounter, returning smoothly to standard bosses at level 6.
|
|
var intro := clampf(float(level - 1) / 5.0, 0.0, 1.0)
|
|
max_hull *= lerpf(0.6, 1.0, intro)
|
|
hull = max_hull
|
|
max_shields = max_hull * 0.4
|
|
shields = max_shields
|
|
weapon_damage *= lerpf(0.5, 1.0, intro)
|
|
weapon_cooldown *= lerpf(1.25, 1.0, intro)
|
|
approach_speed *= lerpf(0.8, 1.0, intro)
|
|
if tier == 2:
|
|
# Independent multipliers preserve the existing intro and shield/hull ratio changes.
|
|
max_hull *= LEVEL_BALANCE.BOSS_HULL_MULTIPLIER
|
|
hull = max_hull
|
|
max_shields *= LEVEL_BALANCE.BOSS_SHIELD_MULTIPLIER
|
|
shields = max_shields
|
|
weapon_damage *= LEVEL_BALANCE.BOSS_DAMAGE_MULTIPLIER
|
|
if level == 1:
|
|
weapon_damage *= LEVEL_BALANCE.FIRST_BOSS_DAMAGE_MULTIPLIER
|
|
weapon_cooldown *= LEVEL_BALANCE.FIRST_BOSS_FIRE_INTERVAL_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 regenerate_defenses(delta: float) -> void:
|
|
if loot_tier == 2 or hull <= 0.0 or is_queued_for_deletion():
|
|
return
|
|
hull = minf(max_hull, hull + max_hull * 0.01 * delta)
|
|
if not system_disabled(2) and _shield_regen_lock <= 0.0: shields = minf(max_shields, shields + max_shields * 0.01 * delta)
|
|
if is_instance_valid(_shield_visual):
|
|
_shield_visual.set_shield_values(shields, max_shields)
|
|
|
|
func tick_shield_technologies(delta: float) -> void:
|
|
_shield_regen_lock = maxf(0.0,_shield_regen_lock-delta)
|
|
_shield_overload_cooldown = maxf(0.0,_shield_overload_cooldown-delta)
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
tick_shield_technologies(delta)
|
|
if _hull_breach_remaining > 0.0: queue_redraw()
|
|
_hull_breach_remaining = maxf(0.0,_hull_breach_remaining-delta)
|
|
tick_disabled_systems(delta)
|
|
regenerate_defenses(delta)
|
|
_slow_remaining = maxf(0.0, _slow_remaining - delta)
|
|
_weapon_timer = maxf(0.0, _weapon_timer - delta)
|
|
if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):
|
|
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
|
|
move_and_slide()
|
|
return
|
|
|
|
if not virus_controlled and hull > 0.0 and hull < max_hull * 0.1:
|
|
_player.call("try_control_enemy", self)
|
|
_combat_target = select_combat_target()
|
|
if _combat_target == null:
|
|
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
|
|
move_and_slide()
|
|
return
|
|
var to_player := _combat_target.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 fleeing := is_retreating()
|
|
if fleeing and not _retreat_mine_rolled:
|
|
_retreat_mine_rolled = true
|
|
if randf() < 0.15:
|
|
_drop_retreat_mine(to_player.normalized())
|
|
var movement := combat_movement(to_player)
|
|
var desired_rotation := (movement if fleeing else direction).angle() + PI * 0.5
|
|
rotation = rotate_toward(rotation, desired_rotation, turn_speed * delta)
|
|
var desired_velocity := Vector2.ZERO if system_disabled(0) else movement * approach_speed * (0.5 if fleeing else 1.0) * get_slow_multiplier()
|
|
velocity = velocity.move_toward(desired_velocity, approach_acceleration * delta)
|
|
if not fleeing and to_player.length() <= weapon_range and _weapon_timer <= 0.0:
|
|
_fire_plasma(direction)
|
|
_weapon_timer = get_fire_interval()
|
|
else:
|
|
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
|
|
|
|
move_and_slide()
|
|
_check_player_collision()
|
|
|
|
func is_retreating() -> bool:
|
|
return not virus_controlled and hull > 0.0 and hull < max_hull * 0.1
|
|
|
|
func _drop_retreat_mine(toward_target: Vector2) -> void:
|
|
var mine := Node2D.new()
|
|
mine.set_script(preload("res://scripts/game/enemy_mine.gd"))
|
|
mine.set("player", _player)
|
|
mine.set("drift", toward_target * 36.0)
|
|
mine.position = position + toward_target * (get_visual_size() * 0.5 + 16.0)
|
|
get_parent().add_child.call_deferred(mine)
|
|
|
|
func combat_movement(to_target: Vector2) -> Vector2:
|
|
var distance := to_target.length()
|
|
var direction := to_target.normalized()
|
|
if is_retreating():
|
|
var escape := -direction
|
|
var half := preload("res://scripts/levels/system_generator.gd").WORLD_SIZE * 0.5 - Vector2(120, 120)
|
|
if absf(global_position.x) > half.x:
|
|
escape.x = -signf(global_position.x)
|
|
if absf(global_position.y) > half.y:
|
|
escape.y = -signf(global_position.y)
|
|
return escape.normalized()
|
|
if tactic == Tactic.RAM:
|
|
return direction
|
|
var preferred := minf(weapon_range * 0.65, 420.0)
|
|
if distance < preferred * 0.8:
|
|
return -direction
|
|
if distance > preferred * 1.15:
|
|
return direction
|
|
return direction.orthogonal() * _orbit_direction * 0.55
|
|
|
|
func get_slow_multiplier() -> float:
|
|
return 0.5 if _slow_remaining > 0.0 else 1.0
|
|
|
|
func select_combat_target() -> Node2D:
|
|
if virus_controlled:
|
|
var opponent: Node2D
|
|
var nearest := INF
|
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
|
if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0:
|
|
continue
|
|
var distance := global_position.distance_squared_to(enemy.global_position)
|
|
if distance < nearest:
|
|
nearest = distance
|
|
opponent = enemy
|
|
return opponent
|
|
var closest: Node2D
|
|
var closest_distance := INF
|
|
for flare in get_tree().get_nodes_in_group("player_flares"):
|
|
if flare.is_queued_for_deletion():
|
|
continue
|
|
var distance := global_position.distance_to(flare.global_position)
|
|
if distance <= maxf(activation_range, weapon_range) and distance < closest_distance:
|
|
closest = flare
|
|
closest_distance = distance
|
|
if closest != null:
|
|
return closest
|
|
var player_visible := is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked"))
|
|
var nearby_range := maxf(activation_range, weapon_range)
|
|
if player_visible and global_position.distance_to(_player.global_position) <= nearby_range:
|
|
return _player
|
|
# Infected ships become fallback targets only when the player is absent nearby.
|
|
for ally in get_tree().get_nodes_in_group("virus_allies"):
|
|
if ally.is_queued_for_deletion() or float(ally.get("hull")) <= 0.0:
|
|
continue
|
|
var distance := global_position.distance_to(ally.global_position)
|
|
if distance <= nearby_range and distance < closest_distance:
|
|
closest = ally
|
|
closest_distance = distance
|
|
if closest != null:
|
|
return closest
|
|
return _player if player_visible else null
|
|
|
|
func apply_mine_hit() -> void:
|
|
if is_queued_for_deletion() or hull <= 0.0:
|
|
return
|
|
_slow_remaining = 5.0
|
|
velocity = velocity.limit_length(approach_speed * 0.5)
|
|
take_damage(hull * 0.5, false, DamageTypes.Type.KINETIC, true)
|
|
|
|
func _check_player_collision() -> void:
|
|
if virus_controlled:
|
|
return
|
|
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:
|
|
if system_disabled(1): return
|
|
direction = research_aim_direction(direction)
|
|
var shot_mask := 37 if virus_controlled else 115
|
|
if not virus_controlled and preload("res://scripts/planets/alien_research.gd").learned("weapons_b_next_next_next"): shot_mask |= 4
|
|
var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0)
|
|
if weapon_kind == ItemDefinition.WeaponKind.LASER:
|
|
var endpoint := muzzle + _laser_aim_direction(direction) * weapon_range
|
|
var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, shot_mask, [get_rid()])
|
|
var hit := get_world_2d().direct_space_state.intersect_ray(query)
|
|
hit = preload("res://scripts/game/shield_intersections.gd").intercept(hit,muzzle,endpoint,shot_mask,[get_rid()],get_tree())
|
|
if not hit.is_empty():
|
|
endpoint = hit["position"]
|
|
var target: Node = hit.get("collider")
|
|
if target is CharacterBody2D and target.has_method("take_damage"):
|
|
preload("res://scripts/game/combat_vfx.gd").mark_hit(target,endpoint,(endpoint-muzzle).normalized())
|
|
var contact_effect := IMPACT_EFFECT_SCENE.instantiate()
|
|
contact_effect.position = endpoint
|
|
get_parent().add_child(contact_effect)
|
|
contact_effect.configure(false,Color("75dfff"))
|
|
contact_effect.configure_contact(DamageTypes.Type.ENERGY,(endpoint-muzzle).normalized())
|
|
if target.has_method("take_enemy_weapon_hit"):
|
|
target.take_enemy_weapon_hit(get_shot_damage(), false, DamageTypes.Type.ENERGY, self)
|
|
else:
|
|
target.take_damage(get_shot_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.source_ship = self
|
|
projectile.collision_mask = shot_mask
|
|
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, get_shot_damage(), weapon_range)
|
|
get_parent().add_child(projectile)
|
|
WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, weapon_kind, -4.0)
|
|
|
|
func take_piercing_weapon_hit(amount: float, critical: bool, damage_type: int, piercing: float) -> void:
|
|
_weapon_piercing = clampf(piercing,0.0,1.0)
|
|
take_weapon_hit(amount,critical,damage_type)
|
|
_weapon_piercing = 0.0
|
|
|
|
func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTypes.Type.KINETIC) -> void:
|
|
_take_player_hit(amount,critical,damage_type,true,-2)
|
|
|
|
func take_research_weapon_hit(amount: float, critical: bool, damage_type: int, piercing: float, disabled_system: int, damage_scale := 1.0) -> void:
|
|
_research_damage_scale = damage_scale
|
|
_weapon_piercing = clampf(piercing,0.0,1.0)
|
|
_take_player_hit(amount,critical,damage_type,true,disabled_system)
|
|
_weapon_piercing = 0.0
|
|
_research_damage_scale = 1.0
|
|
|
|
func take_research_dot(amount: float, damage_type: int, piercing: float) -> void:
|
|
_weapon_piercing = clampf(piercing,0.0,0.5)
|
|
_take_player_hit(amount,false,damage_type,false,-1)
|
|
_weapon_piercing = 0.0
|
|
|
|
func _take_player_hit(amount: float, critical: bool, damage_type: int, direct: bool, disabled_system: int) -> void:
|
|
if is_queued_for_deletion() or hull <= 0.0 or amount <= 0.0:
|
|
return
|
|
var before := hull + shields
|
|
_research_player_hit = true
|
|
var research := preload("res://scripts/planets/alien_research.gd")
|
|
if direct and amount > 0 and research.learned("weapons_a"): amount += 5.0*_research_damage_scale
|
|
_research_direct_hit = direct
|
|
take_damage(amount, critical, damage_type)
|
|
_research_direct_hit = false
|
|
_research_player_hit = false
|
|
if direct and hull > 0 and not is_queued_for_deletion():
|
|
var chosen := preload("res://scripts/game/alien_weapon_effects.gd").roll_disabled_system() if disabled_system == -2 else disabled_system
|
|
if chosen >= 0: disable_system(chosen)
|
|
if is_instance_valid(_player):
|
|
_player.call("restore_shields_from_damage", maxf(0.0, before - hull - shields))
|
|
|
|
func system_disabled(system: int) -> bool:
|
|
return system >= 0 and system < 3 and _disabled_systems[system] > 0.0
|
|
|
|
func disable_system(system: int) -> void:
|
|
if system < 0 or system >= 3 or hull <= 0 or is_queued_for_deletion(): return
|
|
if system == 2 and not system_disabled(2):
|
|
_offline_shields = shields
|
|
shields = 0.0
|
|
_disabled_systems[system] = preload("res://scripts/planets/alien_research.gd").SYSTEM_DISABLE_DURATION
|
|
if is_instance_valid(_shield_visual): _shield_visual.set_shield_values(shields,max_shields)
|
|
|
|
func tick_disabled_systems(delta: float) -> void:
|
|
var shields_offline := system_disabled(2)
|
|
for system in 3: _disabled_systems[system] = maxf(0.0,_disabled_systems[system]-delta)
|
|
if shields_offline and not system_disabled(2) and hull > 0:
|
|
shields = minf(max_shields,_offline_shields)
|
|
_offline_shields = 0.0
|
|
if is_instance_valid(_shield_visual): _shield_visual.set_shield_values(shields,max_shields)
|
|
|
|
func research_aim_direction(direction: Vector2, roll := -1.0, side := 0.0) -> Vector2:
|
|
if virus_controlled or not preload("res://scripts/planets/alien_research.gd").learned("weapons_b_next_next"): return direction
|
|
if (randf() if roll < 0 else roll) >= 0.25: return direction
|
|
var angle := randf_range(0.20,0.45)
|
|
return direction.rotated(angle*(signf(side) if side != 0.0 else (-1.0 if randf()<0.5 else 1.0)))
|
|
|
|
func take_damage(amount: float, critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC, bypass_absorption: bool = false) -> void:
|
|
var visual_contact := preload("res://scripts/game/combat_vfx.gd").consume(self)
|
|
if is_queued_for_deletion() or hull <= 0.0:
|
|
return
|
|
var research := preload("res://scripts/planets/alien_research.gd")
|
|
var absorption_factor := 0.5 if _research_player_hit and _hull_breach_remaining > 0.0 else 1.0
|
|
var actual := maxf(0.0, amount) * (1.0 if bypass_absorption else 1.0 - absorption[damage_type]*absorption_factor / 100.0)
|
|
var progress: Resource = GameState.captain.expedition
|
|
var shield_multiplier := 1.0 + (research.bonus(progress, "shields") if _research_player_hit else 0.0)
|
|
if _research_player_hit and research.learned("shields_a"): shield_multiplier += 0.05
|
|
var hull_multiplier := 1.0 + (research.bonus(progress, "hull") if _research_player_hit else 0.0)
|
|
if _research_player_hit and research.learned("hull_a"): hull_multiplier += 0.05
|
|
var piercing_damage := actual * _weapon_piercing if _research_player_hit else 0.0
|
|
var shield_damage := minf(shields, (actual - piercing_damage) * shield_multiplier)
|
|
var double_hit := _research_direct_hit and research.learned("weapons_a_next_next_next")
|
|
if double_hit and _weapon_piercing < 1.0: shield_damage = minf(shields,actual*shield_multiplier)
|
|
var remaining := actual if double_hit else maxf(0.0, actual - shield_damage / shield_multiplier)
|
|
var shields_before := shields
|
|
shields -= shield_damage
|
|
var hull_damage := minf(hull, remaining * hull_multiplier)
|
|
hull = maxf(0.0, hull - hull_damage)
|
|
if _research_direct_hit and hull_damage > 0.0:
|
|
if shields <= 0.0 and research.learned("hull_a_next"):
|
|
_hull_breach_remaining = 5.0
|
|
queue_redraw()
|
|
var execution_threshold := 0.02 if loot_tier == 2 else 0.08
|
|
if research.learned("hull_a_next_next_next") and hull > 0.0 and hull <= max_hull*execution_threshold:
|
|
hull_damage += hull
|
|
hull = 0.0
|
|
_execution_finish = true
|
|
actual = shield_damage + hull_damage
|
|
if _research_player_hit and shield_damage > 0.0:
|
|
if research.learned("shields_a_next"): _shield_regen_lock = 5.0
|
|
if shields_before > 0.0 and shields <= 0.0 and research.learned("shields_a_next_next") and _shield_overload_cooldown <= 0.0:
|
|
_shield_overload_cooldown = 10.0
|
|
_shield_overload(amount*4.0,damage_type)
|
|
if is_instance_valid(_shield_visual):
|
|
_shield_visual.set_shield_values(shields, max_shields)
|
|
preload("res://scripts/game/combat_vfx.gd").feedback(self,shield_damage,hull_damage,damage_type,visual_contact)
|
|
_show_damage_popup(actual, critical)
|
|
if hull <= 0.0:
|
|
if not _reward_claimed:
|
|
_reward_claimed = true
|
|
_reward_captain()
|
|
_drop_loot()
|
|
_spawn_destruction_effect()
|
|
queue_free()
|
|
|
|
func _shield_overload(damage: float, damage_type: int) -> void:
|
|
var effect := Node2D.new()
|
|
effect.set_script(preload("res://scripts/game/shield_research_effect.gd"))
|
|
effect.position = global_position
|
|
get_parent().add_child(effect)
|
|
for other in get_tree().get_nodes_in_group("enemies"):
|
|
if other == self or other.is_queued_for_deletion() or float(other.get("hull")) <= 0.0: continue
|
|
if global_position.distance_to(other.global_position) <= 120.0:
|
|
# Raw damage deliberately bypasses research triggers and player-hit chains.
|
|
other.take_damage(damage,false,damage_type)
|
|
|
|
func get_fire_interval() -> float:
|
|
if weapon_kind == ItemDefinition.WeaponKind.LASER:
|
|
return maxf(LASER_MINIMUM_COOLDOWN, weapon_cooldown * LASER_COOLDOWN_MULTIPLIER)
|
|
return weapon_cooldown
|
|
|
|
func _laser_aim_direction(direction: Vector2) -> Vector2:
|
|
if randf() < LASER_MISS_CHANCE:
|
|
var side := Vector2(-direction.y, direction.x)
|
|
var distance := global_position.distance_to(_combat_target.global_position) if is_instance_valid(_combat_target) else weapon_range
|
|
var offset := randf_range(60.0, 110.0) * (-1.0 if randf() < 0.5 else 1.0)
|
|
return (direction * maxf(1.0, distance) + side * offset).normalized()
|
|
return direction.rotated(randf_range(-0.02, 0.02))
|
|
|
|
func get_shot_damage() -> float:
|
|
return weapon_damage * (LASER_DAMAGE_MULTIPLIER if weapon_kind == ItemDefinition.WeaponKind.LASER else 1.0) * (1.0 - preload("res://scripts/planets/alien_research.gd").bonus(GameState.captain.expedition, "weapons"))
|
|
|
|
func get_weapon_dps() -> float:
|
|
var interval := get_fire_interval()
|
|
return get_shot_damage() / maxf(0.001, interval) * (1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, interval) 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:
|
|
# Roll independently of equipment drops.
|
|
var distant := not GameState.active_distant_map.is_empty()
|
|
if distant: preload("res://scripts/game/distant_loot.gd").drop(self)
|
|
var bank_chance: float = preload("res://scripts/game/distant_loot.gd").BANK_CHANCE if distant else 0.1
|
|
if randf() < bank_chance:
|
|
var bank := ItemInstance.new()
|
|
bank.definition = preload("res://resources/items/energy_bank.tres")
|
|
var bank_pickup := Node2D.new()
|
|
bank_pickup.set_script(LOOT_PICKUP)
|
|
bank_pickup.set("item", bank)
|
|
bank_pickup.position = position + Vector2(0, -42)
|
|
get_parent().call_deferred("add_child", bank_pickup)
|
|
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_loot(GameState.system_level, loot_tier, null, float(_player.get("luck_percent")) if is_instance_valid(_player) else 0.0))
|
|
pickup.position = position + LOOT_OFFSET
|
|
get_parent().call_deferred("add_child", pickup)
|
|
|
|
func _spawn_destruction_effect() -> void:
|
|
if _execution_finish and _sprite.texture != null:
|
|
var collapse := Node2D.new()
|
|
collapse.set_script(preload("res://scripts/game/hull_collapse_effect.gd"))
|
|
collapse.configure(_sprite.texture,get_visual_size(),global_rotation)
|
|
collapse.position = position
|
|
get_parent().add_child(collapse)
|
|
return
|
|
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
|