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/data/spawn_stages.json b/data/spawn_stages.json index fc4350e..45066d6 100644 --- a/data/spawn_stages.json +++ b/data/spawn_stages.json @@ -1,26 +1,28 @@ [ { - "time_start": 0, - "time_end": 60, - "entries": [ - { "enemy": "res://scenes/slime.tscn", "count_at_start": 0, "count_at_end": 15, "min_interval": 0.5 } - ] + "time_start": 0, + "time_end": 60, + "elite_enemy": "res://scenes/slime.tscn", + "entries": [ + { "enemy": "res://scenes/slime.tscn", "count_at_start": 0, "count_at_end": 15, "min_interval": 0.5 } + ] }, { - "time_start": 60, - "time_end": 180, - "entries": [ - { "enemy": "res://scenes/slime.tscn", "count_at_start": 15, "count_at_end": 40, "min_interval": 0.3 }, - { "enemy": "res://scenes/blue_slime.tscn", "count_at_start": 0, "count_at_end": 10, "min_interval": 0.8 } - ] + "time_start": 60, + "time_end": 180, + "elite_enemy": "res://scenes/blue_slime.tscn", + "entries": [ + { "enemy": "res://scenes/slime.tscn", "count_at_start": 15, "count_at_end": 40, "min_interval": 0.3 }, + { "enemy": "res://scenes/blue_slime.tscn", "count_at_start": 0, "count_at_end": 10, "min_interval": 0.8 } + ] }, { - "time_start": 180, - "time_end": -1, - "entries": [ - { "enemy": "res://scenes/slime.tscn", "count_at_start": 40, "count_at_end": 100, "min_interval": 0.2 }, - { "enemy": "res://scenes/blue_slime.tscn", "count_at_start": 10, "count_at_end": 60, "min_interval": 0.5 }, - { "enemy": "res://scenes/fire_slime.tscn", "count_at_start": 0, "count_at_end": 40, "min_interval": 0.6 } - ] + "time_start": 180, + "time_end": -1, + "entries": [ + { "enemy": "res://scenes/slime.tscn", "count_at_start": 40, "count_at_end": 100, "min_interval": 0.2 }, + { "enemy": "res://scenes/blue_slime.tscn", "count_at_start": 10, "count_at_end": 60, "min_interval": 0.5 }, + { "enemy": "res://scenes/fire_slime.tscn", "count_at_start": 0, "count_at_end": 40, "min_interval": 0.6 } + ] } ] diff --git a/project.godot b/project.godot index d1985a5..173ee6f 100644 --- a/project.godot +++ b/project.godot @@ -34,6 +34,33 @@ window/stretch/mode="viewport" 3d/physics_engine="Jolt Physics" +[input] + +move_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +] +} +move_up={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +] +} +move_down={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} + [rendering] rendering_device/driver.windows="d3d12" diff --git a/scenes/beam.tscn b/scenes/beam.tscn new file mode 100644 index 0000000..685ca99 --- /dev/null +++ b/scenes/beam.tscn @@ -0,0 +1,122 @@ +[gd_scene format=3 uid="uid://d2mpjsuueg0bn"] + +[ext_resource type="Script" uid="uid://bdhx27edemfce" path="res://scripts/beam.gd" id="1_beam00"] +[ext_resource type="Texture2D" uid="uid://dxox0vjihmukh" path="res://assets/Fire Pixel Bullet 16x16/All_Fire_Bullet_Pixel_16x16_05.png" id="2_hl8vi"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_daiji"] +atlas = ExtResource("2_hl8vi") +region = Rect2(96, 0, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_bawts"] +atlas = ExtResource("2_hl8vi") +region = Rect2(112, 0, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_8wlve"] +atlas = ExtResource("2_hl8vi") +region = Rect2(128, 0, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_bt3ye"] +atlas = ExtResource("2_hl8vi") +region = Rect2(144, 0, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_a06w7"] +atlas = ExtResource("2_hl8vi") +region = Rect2(96, 16, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_5fvdj"] +atlas = ExtResource("2_hl8vi") +region = Rect2(112, 16, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_djxf5"] +atlas = ExtResource("2_hl8vi") +region = Rect2(128, 16, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_eeead"] +atlas = ExtResource("2_hl8vi") +region = Rect2(144, 16, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_qfoqh"] +atlas = ExtResource("2_hl8vi") +region = Rect2(96, 32, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_tuykp"] +atlas = ExtResource("2_hl8vi") +region = Rect2(112, 32, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_2dcgn"] +atlas = ExtResource("2_hl8vi") +region = Rect2(128, 32, 16, 16) + +[sub_resource type="AtlasTexture" id="AtlasTexture_7703o"] +atlas = ExtResource("2_hl8vi") +region = Rect2(144, 32, 16, 16) + +[sub_resource type="SpriteFrames" id="SpriteFrames_beam0"] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_daiji") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_bawts") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_8wlve") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_bt3ye") +}], +"loop": true, +"name": &"end", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_a06w7") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_5fvdj") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_djxf5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_eeead") +}], +"loop": true, +"name": &"middle", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_qfoqh") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_tuykp") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_2dcgn") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_7703o") +}], +"loop": true, +"name": &"start", +"speed": 10.0 +}] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_beam0"] +size = Vector2(16, 8) + +[node name="Beam" type="Area2D" unique_id=968931754] +script = ExtResource("1_beam00") + +[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="." unique_id=100000001] +texture_filter = 1 +sprite_frames = SubResource("SpriteFrames_beam0") +animation = &"end" +frame_progress = 0.02647079 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=100000002] +shape = SubResource("RectangleShape2D_beam0") +disabled = true diff --git a/scenes/chili.tscn b/scenes/chili.tscn new file mode 100644 index 0000000..4c09532 --- /dev/null +++ b/scenes/chili.tscn @@ -0,0 +1,22 @@ +[gd_scene format=3 uid="uid://cchili0scene1"] + +[ext_resource type="Script" path="res://scripts/chili.gd" id="1_chili0"] +[ext_resource type="Texture2D" uid="uid://d2pinnrigixnp" path="res://assets/16x16 Pixelart Food Icons/Pixel_Foods(ARTLİNE).png" id="2_chili0"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_chili"] +atlas = ExtResource("2_chili0") +region = Rect2(19, 37, 16, 16) + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_chili"] +radius = 4.0 +height = 12.0 + +[node name="Chili" type="Area2D" unique_id=200000001] +script = ExtResource("1_chili0") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=200000002] +texture_filter = 1 +texture = SubResource("AtlasTexture_chili") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=200000003] +shape = SubResource("CapsuleShape2D_chili") diff --git a/scenes/fire_slime.tscn b/scenes/fire_slime.tscn index 7443feb..df2b5d1 100644 --- a/scenes/fire_slime.tscn +++ b/scenes/fire_slime.tscn @@ -1,6 +1,8 @@ [gd_scene format=3 uid="uid://cpe6aiuqiox0u"] [ext_resource type="Script" uid="uid://dklt42vjjcks7" path="res://scripts/fire_slime.gd" id="1_88j2t"] +[ext_resource type="Script" uid="uid://cjkaw7wqw4e30" path="res://scripts/drop_table.gd" id="dt_fslime"] +[ext_resource type="PackedScene" uid="uid://cchili0scene1" path="res://scenes/chili.tscn" id="chili_fslime"] [ext_resource type="Texture2D" uid="uid://b1tyfy8ooudkc" path="res://assets/Slime3/With_shadow/Slime3_Death_with_shadow.png" id="2_ahfdi"] [ext_resource type="Texture2D" uid="uid://bbm1sv6hmc2j" path="res://assets/Slime3/With_shadow/Slime3_Hurt_with_shadow.png" id="3_kq38e"] [ext_resource type="Texture2D" uid="uid://bt07131sttb6e" path="res://assets/Slime3/With_shadow/Slime3_Walk_with_shadow.png" id="4_ret1g"] @@ -732,9 +734,15 @@ radius = 7.071068 [sub_resource type="CircleShape2D" id="CircleShape2D_odbmi"] radius = 8.062258 +[sub_resource type="Resource" id="Resource_chili_drop"] +script = ExtResource("dt_fslime") +drop = ExtResource("chili_fslime") +chance = 0.4 + [node name="FireSlime" type="CharacterBody2D" unique_id=1827403107] script = ExtResource("1_88j2t") metadata/_custom_type_script = "uid://c0uv02nt5ocvg" +drop_table = Array[ExtResource("dt_fslime")]([SubResource("Resource_chili_drop")]) [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="." unique_id=1151813585] texture_filter = 1 diff --git a/scenes/laser.tscn b/scenes/laser.tscn new file mode 100644 index 0000000..edeb025 --- /dev/null +++ b/scenes/laser.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3 uid="uid://claser0spell1"] + +[ext_resource type="Script" path="res://scripts/laser.gd" id="1_laser0"] + +[node name="Laser" type="Node2D"] +script = ExtResource("1_laser0") diff --git a/scripts/DropsBase.gd b/scripts/DropsBase.gd index 855c6ba..9c6a9aa 100644 --- a/scripts/DropsBase.gd +++ b/scripts/DropsBase.gd @@ -4,6 +4,7 @@ class_name DropsBase var witch var player var is_spawning = true +var in_orbit: bool = false signal collected var _collect_sfx = preload("res://assets/music&sfx/sfx/lesiakower-coin-collect-retro-8-bit-sound-effect-145251.mp3") @@ -44,9 +45,14 @@ func _animate_spawn() -> void: is_spawning = false func _on_body_entered(body: Node2D) -> void: - if body == player and not is_spawning: - collect() - pass + if body == player and not is_spawning and not in_orbit: + var cauldron = witch.get_node("CauldronBar") + if cauldron.is_brewing and player.fruit_queue.size() < 3: + _sfx_player.pitch_scale = randf_range(0.85, 1.15) + _sfx_player.play() + player.add_to_queue(self) + else: + collect() func collect(): _sfx_player.pitch_scale = randf_range(0.85, 1.15) diff --git a/scripts/SpawnStage.gd b/scripts/SpawnStage.gd index 186b8f7..5ac3e26 100644 --- a/scripts/SpawnStage.gd +++ b/scripts/SpawnStage.gd @@ -2,5 +2,6 @@ extends Resource class_name SpawnStage @export var time_start: float = 0.0 -@export var time_end: float = -1.0 # -1 = forever +@export var time_end: float = -1.0 +@export var elite_enemy: PackedScene = null @export var entries: Array[StageEntry] diff --git a/scripts/SpellLibrary.gd b/scripts/SpellLibrary.gd index 88d06b0..c611c9d 100644 --- a/scripts/SpellLibrary.gd +++ b/scripts/SpellLibrary.gd @@ -2,29 +2,31 @@ extends Node const APPLE = 0 const GRAPE = 1 +const CHILI = 2 const NONE = "NONE" const SHURIKEN = "SHURIKEN" const FIREBALL = "FIREBALL" const FIRE_SWIRL = "FIRE_SWIRL" const TORNADO = "TORNADO" +const LASER = "LASER" -# Each spell's display recipe: sorted apples-first, grapes-last var recipes: Dictionary = { - SHURIKEN: [APPLE, GRAPE, GRAPE], - FIREBALL: [APPLE, APPLE, APPLE], - FIRE_SWIRL: [APPLE, APPLE, GRAPE], - TORNADO: [GRAPE, GRAPE, GRAPE], + FIREBALL: [APPLE], + TORNADO: [GRAPE], + LASER: [CHILI], + SHURIKEN: [APPLE, GRAPE], + FIRE_SWIRL: [APPLE, CHILI], } -# Takes cauldron slot_states (uses texture indices: 2 = apple, 4 = grape) -# and returns which spell that combination brews. func identify(cauldron_slots: Array) -> String: - var apples = cauldron_slots.count(2) - var grapes = cauldron_slots.count(4) + var types: Array = [] + if cauldron_slots.any(func(x): return x == 2): types.append(APPLE) + if cauldron_slots.any(func(x): return x == 4): types.append(GRAPE) + if cauldron_slots.any(func(x): return x == 3): types.append(CHILI) + for spell_id in recipes: - var r: Array = recipes[spell_id] - if r.count(APPLE) == apples and r.count(GRAPE) == grapes: + if recipes[spell_id] == types: return spell_id return NONE diff --git a/scripts/beam.gd b/scripts/beam.gd new file mode 100644 index 0000000..90fd51f --- /dev/null +++ b/scripts/beam.gd @@ -0,0 +1,11 @@ +extends Area2D + +@export var beam_type := "middle" + +@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D +@onready var shape: CollisionShape2D = $CollisionShape2D + +func _ready() -> void: + if sprite.sprite_frames.has_animation(beam_type): + sprite.play(beam_type) + shape.disabled = beam_type != "middle" diff --git a/scripts/beam.gd.uid b/scripts/beam.gd.uid new file mode 100644 index 0000000..9d496cd --- /dev/null +++ b/scripts/beam.gd.uid @@ -0,0 +1 @@ +uid://bdhx27edemfce diff --git a/scripts/blue_slime.gd b/scripts/blue_slime.gd index 944a478..6f1c3cb 100644 --- a/scripts/blue_slime.gd +++ b/scripts/blue_slime.gd @@ -5,6 +5,7 @@ func _ready() -> void: speed = 15.0 max_hp = 25 hp = max_hp + damage = 5 # $Area2D.body_entered.connect(_on_area_2d_body_entered) func _process(delta: float) -> void: diff --git a/scripts/cauldron_bar.gd b/scripts/cauldron_bar.gd index 7965f73..6553097 100644 --- a/scripts/cauldron_bar.gd +++ b/scripts/cauldron_bar.gd @@ -13,6 +13,7 @@ var progres_index = 0 var is_brewing var brew_explosion = true @onready var witch = get_parent() +@onready var player = get_node("/root/Game/Player") var explosion_scene = preload("res://scenes/explosion.tscn") var _ignite_sfx = preload("res://assets/music&sfx/sfx/data_pion-sfx9-fwoosh-324525.mp3") var _ignite_player: AudioStreamPlayer @@ -87,6 +88,8 @@ func progres_bar(fruit): change_texture(progres_index, 2) if fruit is Grape: change_texture(progres_index, 4) + if fruit is Chili: + change_texture(progres_index, 3) _pop_slot(progres_index) progres_index += 1 if progres_index == 3: @@ -117,9 +120,11 @@ func brew(fruits): SpellLibrary.FIREBALL: witch.shoot_fireballs() SpellLibrary.FIRE_SWIRL: witch.shoot_fire_swirl() SpellLibrary.TORNADO: witch.shoot_tornado() + SpellLibrary.LASER: witch.shoot_laser() reset_texture() is_brewing = false - + player.flush_queue() + func get_unique_fruits() -> Array: var unique = [] for fruit in slot_states: diff --git a/scripts/chili.gd b/scripts/chili.gd new file mode 100644 index 0000000..1540cd9 --- /dev/null +++ b/scripts/chili.gd @@ -0,0 +1,2 @@ +extends DropsBase +class_name Chili diff --git a/scripts/chili.gd.uid b/scripts/chili.gd.uid new file mode 100644 index 0000000..5fc624b --- /dev/null +++ b/scripts/chili.gd.uid @@ -0,0 +1 @@ +uid://dinqfnri3co88 diff --git a/scripts/debug_menu.gd b/scripts/debug_menu.gd index 09dc8e9..3cefc53 100644 --- a/scripts/debug_menu.gd +++ b/scripts/debug_menu.gd @@ -3,6 +3,13 @@ extends CanvasLayer @onready var _spawn_control = get_node("/root/Game/SpawnControl") @onready var _witch = get_node("/root/Game/Witch") @onready var _drop_manager = get_node("/root/Game/DropManager") +@onready var _player = get_node("/root/Game/Player") + +var _fruit_scenes: Dictionary = { + "Apple": preload("res://scenes/apple.tscn"), + "Grape": preload("res://scenes/grape.tscn"), + "Chili": preload("res://scenes/chili.tscn"), +} var _spell_dispatch: Dictionary var _lvl_disable_btn: Button @@ -14,6 +21,7 @@ func _ready() -> void: SpellLibrary.SHURIKEN: _witch.shoot_shuriken, SpellLibrary.FIRE_SWIRL: _witch.shoot_fire_swirl, SpellLibrary.TORNADO: _witch.shoot_tornado, + SpellLibrary.LASER: _witch.shoot_laser, } _build_ui() hide() @@ -72,6 +80,10 @@ func _build_ui() -> void: btn.pressed.connect(_skip_time.bind(float(secs))) hbox_time.add_child(btn) + _add_section(vbox, "FRUITS") + for fruit_name in _fruit_scenes.keys(): + _add_button(vbox, fruit_name, _spawn_fruit.bind(fruit_name)) + _add_section(vbox, "SPELLS") for spell_id in SpellLibrary.recipes.keys(): if _spell_dispatch.has(spell_id): @@ -99,6 +111,11 @@ func _add_button(parent: VBoxContainer, label: String, callback: Callable) -> vo btn.pressed.connect(callback) parent.add_child(btn) +func _spawn_fruit(fruit_name: String) -> void: + var fruit = _fruit_scenes[fruit_name].instantiate() + fruit.global_position = _player.global_position + Vector2(randf_range(-30, 30), randf_range(-30, 30)) + get_node("/root/Game").add_child(fruit) + func _kill_all_enemies() -> void: for enemy in get_tree().get_nodes_in_group("enemies"): if is_instance_valid(enemy): diff --git a/scripts/fire_slime.gd b/scripts/fire_slime.gd index 210553a..28424ad 100644 --- a/scripts/fire_slime.gd +++ b/scripts/fire_slime.gd @@ -5,6 +5,7 @@ func _ready() -> void: speed = 15.0 max_hp = 50 hp = max_hp + damage = 10 $Area2D.body_entered.connect(_on_area_2d_body_entered) func _process(delta: float) -> void: diff --git a/scripts/laser.gd b/scripts/laser.gd new file mode 100644 index 0000000..fa56bcc --- /dev/null +++ b/scripts/laser.gd @@ -0,0 +1,140 @@ +extends Node2D + +const BEAM_DURATION := 3.0 +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 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 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 + 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() + if elapsed >= BEAM_DURATION: + done = true + cleanup() + +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) + + var positions: Array = [beam_start] + var types: Array = ["start"] + for i in range(n_mid): + 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") + + 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() + + 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) + + var hit: Array = [] + for seg in mid_segs: + if not is_instance_valid(seg): + continue + for body in seg.get_overlapping_bodies(): + if body.is_in_group("enemies") and not body.is_dying \ + and body != target and not hit.has(body): + body.take_damage(SPLASH_TICK_DMG) + hit.append(body) + +func cleanup() -> void: + for seg in all_segs: + if is_instance_valid(seg): + seg.queue_free() + queue_free() + +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 + if enemy.hp > best_hp: + best_hp = enemy.hp + best = enemy + return best diff --git a/scripts/laser.gd.uid b/scripts/laser.gd.uid new file mode 100644 index 0000000..b174485 --- /dev/null +++ b/scripts/laser.gd.uid @@ -0,0 +1 @@ +uid://dp6b3imslv10d diff --git a/scripts/perk_effects.gd b/scripts/perk_effects.gd index 15bbf1f..caa8584 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 @@ -15,12 +16,14 @@ var _knife_timer: float = 0.0 var spellbook_scene = preload("res://scenes/spellbook.tscn") var spellbook_count: int = 0 var spellbook_damage: int = 12 +const SPELLBOOK_MAX_DAMAGE: int = 40 var spellbook_speed: float = 1.5 const SPELLBOOK_RADIUS: float = 60.0 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,10 +54,23 @@ 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" - dsh.stats = _stat("Shurikens", "1", "2") + dsh.description = "Fire two more shurikens at once" + dsh.stats = _stat("Shurikens", str(witch.shuriken_count), str(witch.shuriken_count + 2)) dsh.spell = SpellLibrary.SHURIKEN dsh.icon = _icon_shuriken dsh.effect = double_shuriken @@ -69,6 +85,15 @@ func _ready() -> void: faoe.effect = fireball_aoe available_perks.append(faoe) + var fsp = Perk.new() + fsp.name = "Fireball Spread" + fsp.description = "Fireballs target more enemies" + fsp.stats = _stat("Targets", str(witch.fireball_max_targets), str(witch.fireball_max_targets + 3)) + fsp.spell = SpellLibrary.FIREBALL + fsp.icon = _icon_fireball + fsp.effect = fireball_spread + available_perks.append(fsp) + var bexp = Perk.new() bexp.name = "Brew Explosion" bexp.description = "Trigger an explosion on brew" @@ -135,12 +160,18 @@ 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 + witch.shuriken_count += 2 func fireball_aoe(): fireball_aoe_enabled = true +func fireball_spread(): + witch.fireball_max_targets += 3 + func brew_explosion(): cauldron.brew_explosion = true @@ -237,16 +268,17 @@ func faster_orbit() -> void: available_perks.append(fo) func book_damage() -> void: - spellbook_damage += 4 + spellbook_damage = min(spellbook_damage + 4, SPELLBOOK_MAX_DAMAGE) for book in _spellbooks: book.damage = spellbook_damage - var bd = Perk.new() - bd.name = "Book Damage" - bd.description = "Books hit harder" - bd.stats = _stat("Damage", str(spellbook_damage), str(spellbook_damage + 4)) - bd.icon = _icon_book - bd.effect = book_damage - available_perks.append(bd) + if spellbook_damage < SPELLBOOK_MAX_DAMAGE: + var bd = Perk.new() + bd.name = "Book Damage" + bd.description = "Books hit harder" + bd.stats = _stat("Damage", str(spellbook_damage), str(min(spellbook_damage + 4, SPELLBOOK_MAX_DAMAGE))) + bd.icon = _icon_book + bd.effect = book_damage + available_perks.append(bd) func _rebuild_spellbooks() -> void: for book in _spellbooks: diff --git a/scripts/player.gd b/scripts/player.gd index 728f896..2d3697d 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -6,11 +6,22 @@ var level = 1 var speed = 60 var strength = 3 var attacks = false +var fruit_queue: Array = [] +var orbit_angle: float = 0.0 +const ORBIT_RADIUS := 20.0 +const ORBIT_SPEED := 2.5 func _physics_process(delta): - var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down") velocity = direction * speed move_and_slide() + orbit_angle += ORBIT_SPEED * delta + for i in fruit_queue.size(): + var fruit = fruit_queue[i] + if is_instance_valid(fruit): + var angle = orbit_angle + (TAU / fruit_queue.size()) * i + var target_pos = global_position + Vector2.from_angle(angle) * ORBIT_RADIUS + fruit.global_position = fruit.global_position.lerp(target_pos, 1.0 - exp(-12.0 * delta)) if attacks == true: return if direction == Vector2.ZERO: @@ -76,6 +87,19 @@ func _on_attack_speed_timeout() -> void: +func add_to_queue(fruit) -> void: + fruit.in_orbit = true + fruit_queue.append(fruit) + +func flush_queue() -> void: + var to_flush = fruit_queue.duplicate() + fruit_queue.clear() + for fruit in to_flush: + if is_instance_valid(fruit): + fruit.in_orbit = false + fruit.collect() + await get_tree().create_timer(0.15).timeout + func _on_melee_area_body_entered(body: Node2D) -> void: if body.is_in_group("enemies"): if $AttackSpeed.is_stopped(): diff --git a/scripts/spawn_control.gd b/scripts/spawn_control.gd index 2f0330b..5101aa3 100644 --- a/scripts/spawn_control.gd +++ b/scripts/spawn_control.gd @@ -8,11 +8,13 @@ var elapsed_time: float = 0.0 const STAGES_JSON = "res://data/spawn_stages.json" -var stages: Array[SpawnStage] = [] +const ELITE_HP_MULT := 20 +const ELITE_SPD_MULT := 0.25 +const ELITE_SCALE := 3.0 -# _state keys: Vector2i(stage_idx, entry_idx) -# values: { "timer": float, "alive": int } +var stages: Array[SpawnStage] = [] var _state: Dictionary = {} +var current_stage_idx: int = -1 func _ready() -> void: var camera: Camera2D = get_parent().get_node("Camera2D") @@ -44,6 +46,8 @@ func _load_stages(path: String) -> void: var stage = SpawnStage.new() stage.time_start = float(sd["time_start"]) stage.time_end = float(sd["time_end"]) + if sd.has("elite_enemy"): + stage.elite_enemy = load(sd["elite_enemy"]) for ed in sd["entries"]: var entry = StageEntry.new() entry.enemy = load(ed["enemy"]) @@ -72,9 +76,31 @@ func get_spawn_position() -> Vector2: spawn_y = randf_range(up_right.y, down_right.y) return Vector2(spawn_x, spawn_y) +func _active_stage_idx() -> int: + for si in stages.size(): + var s: SpawnStage = stages[si] + if elapsed_time >= s.time_start and (s.time_end == -1.0 or elapsed_time < s.time_end): + return si + return stages.size() - 1 + +func _spawn_elite(scene: PackedScene) -> void: + var enemy = scene.instantiate() + enemy.global_position = get_spawn_position() + add_child(enemy) + enemy.scale = Vector2(ELITE_SCALE, ELITE_SCALE) + enemy.max_hp = enemy.max_hp * ELITE_HP_MULT + enemy.hp = enemy.max_hp + enemy.speed *= ELITE_SPD_MULT + func _process(delta: float) -> void: elapsed_time += delta + var new_idx := _active_stage_idx() + if new_idx != current_stage_idx: + if current_stage_idx >= 0 and stages[current_stage_idx].elite_enemy != null: + _spawn_elite(stages[current_stage_idx].elite_enemy) + current_stage_idx = new_idx + for si in stages.size(): var stage: SpawnStage = stages[si] if elapsed_time < stage.time_start: diff --git a/scripts/witch.gd b/scripts/witch.gd index 435d3fb..891ade4 100644 --- a/scripts/witch.gd +++ b/scripts/witch.gd @@ -8,12 +8,15 @@ var fireball = preload("res://scenes/fireball.tscn") var shuriken = preload("res://scenes/shuriken.tscn") 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 +var max_hp: int = 50 +var current_hp: int = 50 var is_invincible: bool = false +var fireball_max_targets: int = 5 const HP_BAR_WIDTH = 20 const HP_BAR_HEIGHT = 3 @@ -51,7 +54,10 @@ func _on_collect(DropsBase): func shoot_fireballs(): var enemies = get_tree().get_nodes_in_group("enemies") - for enemy in enemies: + enemies = enemies.filter(func(e): return is_instance_valid(e)) + enemies.sort_custom(func(a, b): return global_position.distance_to(a.global_position) < global_position.distance_to(b.global_position)) + var targets = enemies.slice(0, fireball_max_targets) + for enemy in targets: if not is_instance_valid(enemy): continue var dir = global_position.direction_to(enemy.global_position) @@ -99,6 +105,19 @@ func shoot_tornado(): tw.global_position = target.global_position if target != null else global_position get_parent().add_child(tw) camera.shake(0.3, 0.8) + +func shoot_laser(): + var ls = laser.instantiate() + 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: @@ -111,7 +130,7 @@ func take_damage(amount: int) -> void: get_tree().call_deferred("reload_current_scene") return is_invincible = true - await get_tree().create_timer(1.0).timeout + await get_tree().create_timer(0.5).timeout is_invincible = false func get_nearest_enemy(from: Vector2, filter: Callable = Callable()) -> Node: