diff --git a/assets/music&sfx/sfx/laser.wav b/assets/music&sfx/sfx/laser.wav new file mode 100644 index 0000000..874f9a1 Binary files /dev/null and b/assets/music&sfx/sfx/laser.wav differ diff --git a/assets/music&sfx/sfx/laser.wav.import b/assets/music&sfx/sfx/laser.wav.import new file mode 100644 index 0000000..c9af200 --- /dev/null +++ b/assets/music&sfx/sfx/laser.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://cn7yee27ivj7b" +path="res://.godot/imported/laser.wav-ff32fed33cb3ccbcd074381583780fec.sample" + +[deps] + +source_file="res://assets/music&sfx/sfx/laser.wav" +dest_files=["res://.godot/imported/laser.wav-ff32fed33cb3ccbcd074381583780fec.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/scripts/laser.gd b/scripts/laser.gd index befebd8..fa56bcc 100644 --- a/scripts/laser.gd +++ b/scripts/laser.gd @@ -1,32 +1,48 @@ extends Node2D const BEAM_DURATION := 3.0 -const PRIMARY_TICK_DMG := 15 +const PRIMARY_TICK_DMG := 40 const SPLASH_TICK_DMG := 3 const TICK_INTERVAL := 0.5 +const SWEEP_RATE := 22.0 -@export var segment_size := 16.0 +@export var segment_size := 16.0 +@export var start_offset := 24.0 + +@onready var perk_effects = get_node("/root/Game/PerkEffects") var beam_seg := preload("res://scenes/beam.tscn") -var target: Node2D = null -var all_segs: Array = [] -var mid_segs: Array = [] -var tick_timer := 0.0 -var elapsed := 0.0 -var done := false +var target: Node2D = null +var all_segs: Array = [] +var mid_segs: Array = [] +var tick_timer := 0.0 +var elapsed := 0.0 +var done := false +var current_angle := 0.0 +var sweep_to := 0.0 +var sweeping := false func _ready() -> void: target = get_highest_hp_enemy() if target == null: queue_free() return - spawn_beam() + current_angle = global_position.direction_to(target.global_position).angle() + rebuild_segments() func _process(delta: float) -> void: if done: return elapsed += delta tick_timer += delta + + if sweeping: + current_angle = lerp_angle(current_angle, sweep_to, 1.0 - exp(-SWEEP_RATE * delta)) + if abs(angle_difference(current_angle, sweep_to)) < 0.005: + current_angle = sweep_to + sweeping = false + rebuild_segments() + if tick_timer >= TICK_INTERVAL: tick_timer -= TICK_INTERVAL do_damage_tick() @@ -34,32 +50,65 @@ func _process(delta: float) -> void: done = true cleanup() -func spawn_beam() -> void: - var origin := global_position - var dest := target.global_position - var dir := origin.direction_to(dest) - var dist := origin.distance_to(dest) - var angle := dir.angle() +func rebuild_segments() -> void: + var dir := Vector2.from_angle(current_angle) + var beam_start := global_position + dir * start_offset + var dest_dist := global_position.distance_to(target.global_position) + var beam_dist := dest_dist - start_offset + var n_mid := int(max(0.0, beam_dist - segment_size) / segment_size) - place_seg("start", origin, angle) - - var n_mid := int(max(0.0, dist - segment_size * 2.0) / segment_size) + var positions: Array = [beam_start] + var types: Array = ["start"] for i in range(n_mid): - var pos := origin + dir * (segment_size * (float(i) + 1.0)) - mid_segs.append(place_seg("middle", pos, angle)) + positions.append(beam_start + dir * (segment_size * float(i + 1))) + types.append("middle") + positions.append(beam_start + dir * (segment_size * float(n_mid + 1))) + types.append("end") - place_seg("end", dest, angle) + while all_segs.size() < positions.size(): + var seg = beam_seg.instantiate() + get_parent().add_child(seg) + all_segs.append(seg) + while all_segs.size() > positions.size(): + var seg = all_segs.pop_back() + if is_instance_valid(seg): + seg.queue_free() -func place_seg(type: String, pos: Vector2, angle: float) -> Node: - var seg = beam_seg.instantiate() - seg.beam_type = type - seg.global_position = pos - seg.rotation = angle - get_parent().add_child(seg) - all_segs.append(seg) - return seg + mid_segs.clear() + for i in range(all_segs.size()): + var seg = all_segs[i] + if not is_instance_valid(seg): + continue + seg.global_position = positions[i] + seg.rotation = current_angle + if seg.beam_type != types[i]: + seg.beam_type = types[i] + seg.shape.disabled = types[i] != "middle" + if seg.sprite.sprite_frames.has_animation(types[i]): + seg.sprite.play(types[i]) + if types[i] == "middle": + mid_segs.append(seg) + + if all_segs.size() > 1 and is_instance_valid(all_segs[0]): + var ref_frame: int = all_segs[0].sprite.frame + var ref_progress: float = all_segs[0].sprite.frame_progress + for i in range(1, all_segs.size()): + var seg = all_segs[i] + if is_instance_valid(seg): + seg.sprite.frame = ref_frame + seg.sprite.frame_progress = ref_progress + +func retarget(new_target: Node2D) -> void: + target = new_target + sweep_to = global_position.direction_to(new_target.global_position).angle() + sweeping = true func do_damage_tick() -> void: + if (not is_instance_valid(target) or target.is_dying) and perk_effects.laser_retarget_enabled: + var new_target = get_highest_hp_enemy() + if new_target != null: + retarget(new_target) + if is_instance_valid(target) and not target.is_dying: target.take_damage(PRIMARY_TICK_DMG) @@ -79,9 +128,9 @@ func cleanup() -> void: seg.queue_free() queue_free() -func get_highest_hp_enemy() -> Node: - var best: Node = null - var best_hp: int = -1 +func get_highest_hp_enemy() -> Node2D: + var best: Node2D = null + var best_hp: int = -1 for enemy in get_tree().get_nodes_in_group("enemies"): if not is_instance_valid(enemy) or enemy.is_dying: continue diff --git a/scripts/perk_effects.gd b/scripts/perk_effects.gd index 15bbf1f..ccd2584 100644 --- a/scripts/perk_effects.gd +++ b/scripts/perk_effects.gd @@ -7,6 +7,7 @@ var throwing_knife = preload("res://scenes/throwing_knive.tscn") var cauldron var available_perks: Array[Perk] = [] var fireball_aoe_enabled = false +var laser_retarget_enabled = false var throwing_knife_enabled = false var throwing_knife_cooldown: float = 2.0 var throwing_knife_count: int = 1 @@ -21,6 +22,7 @@ var _spellbook_angle: float = 0.0 var _spellbooks: Array = [] var _icon_knife = preload("res://assets/weapons/knvie.png") +var _icon_laser: AtlasTexture var _icon_book = preload("res://assets/books_set_2/books_pentagram.png") var _icon_brew = preload("res://assets/books_set_2/books_health_potion.png") var _icon_shuriken: AtlasTexture @@ -51,6 +53,19 @@ func _ready() -> void: _icon_player.atlas = preload("res://assets/Swordsman_lvl1/Without_shadow/Swordsman_lvl1_Idle_without_shadow.png") _icon_player.region = Rect2(0, 0, 64, 64) + _icon_laser = AtlasTexture.new() + _icon_laser.atlas = preload("res://assets/Fire Pixel Bullet 16x16/All_Fire_Bullet_Pixel_16x16_05.png") + _icon_laser.region = Rect2(96, 16, 16, 16) + + var lrt = Perk.new() + lrt.name = "Laser Lock-On" + lrt.description = "Laser retargets to the next highest health enemy on kill" + lrt.stats = _stat_toggle("Retarget") + lrt.spell = SpellLibrary.LASER + lrt.icon = _icon_laser + lrt.effect = laser_retarget + available_perks.append(lrt) + var dsh = Perk.new() dsh.name = "Double Shuriken" dsh.description = "Fire two shurikens at once" @@ -135,6 +150,9 @@ func _process(delta: float) -> void: var offset = (TAU / _spellbooks.size()) * i _spellbooks[i].global_position = witch.global_position + Vector2(cos(_spellbook_angle + offset), sin(_spellbook_angle + offset)) * SPELLBOOK_RADIUS +func laser_retarget(): + laser_retarget_enabled = true + func double_shuriken(): witch.shuriken_count = 2 diff --git a/scripts/witch.gd b/scripts/witch.gd index e6d73c3..1b5be8a 100644 --- a/scripts/witch.gd +++ b/scripts/witch.gd @@ -10,7 +10,8 @@ var fire_swirl = preload("res://scenes/fire_swirl.tscn") var tornado = preload("res://scenes/tornado.tscn") var laser = preload("res://scenes/laser.tscn") var shuriken_count = 1 -var _fire_sfx = preload("res://assets/music&sfx/sfx/fire.wav") +var _fire_sfx = preload("res://assets/music&sfx/sfx/fire.wav") +var _laser_sfx = preload("res://assets/music&sfx/sfx/laser.wav") var max_hp: int = 100 var current_hp: int = 100 @@ -106,6 +107,13 @@ func shoot_laser(): ls.global_position = global_position get_parent().add_child(ls) camera.shake(0.4, 1.2) + var asp = AudioStreamPlayer.new() + asp.stream = _laser_sfx + asp.volume_db = 6 + asp.bus = "SFX" + get_parent().add_child(asp) + asp.play() + asp.finished.connect(asp.queue_free) func take_damage(amount: int) -> void: if is_invincible: