From 24d45d1e9c1a29723592d0f8c683f19bf9fe8e5b Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 15:36:54 +0200 Subject: [PATCH 1/8] add WASD controls --- project.godot | 27 +++++++++++++++++++++++++++ scripts/player.gd | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) 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/scripts/player.gd b/scripts/player.gd index 728f896..d621ab6 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -8,7 +8,7 @@ var strength = 3 var attacks = false 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() if attacks == true: From 89446660c053e85ea0b9a63376374614fe7b46e0 Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 16:12:08 +0200 Subject: [PATCH 2/8] Added Laser Spell, no recipe yet --- scenes/beam.tscn | 122 ++++++++++++++++++++++++++++++++++++++++ scenes/laser.tscn | 6 ++ scripts/SpellLibrary.gd | 2 + scripts/beam.gd | 11 ++++ scripts/beam.gd.uid | 1 + scripts/debug_menu.gd | 1 + scripts/laser.gd | 91 ++++++++++++++++++++++++++++++ scripts/laser.gd.uid | 1 + scripts/witch.gd | 7 +++ 9 files changed, 242 insertions(+) create mode 100644 scenes/beam.tscn create mode 100644 scenes/laser.tscn create mode 100644 scripts/beam.gd create mode 100644 scripts/beam.gd.uid create mode 100644 scripts/laser.gd create mode 100644 scripts/laser.gd.uid 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/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/SpellLibrary.gd b/scripts/SpellLibrary.gd index 88d06b0..24aac26 100644 --- a/scripts/SpellLibrary.gd +++ b/scripts/SpellLibrary.gd @@ -8,6 +8,7 @@ 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 = { @@ -15,6 +16,7 @@ var recipes: Dictionary = { FIREBALL: [APPLE, APPLE, APPLE], FIRE_SWIRL: [APPLE, APPLE, GRAPE], TORNADO: [GRAPE, GRAPE, GRAPE], + LASER: [], } # Takes cauldron slot_states (uses texture indices: 2 = apple, 4 = grape) 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/debug_menu.gd b/scripts/debug_menu.gd index 09dc8e9..6d44c4a 100644 --- a/scripts/debug_menu.gd +++ b/scripts/debug_menu.gd @@ -14,6 +14,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() diff --git a/scripts/laser.gd b/scripts/laser.gd new file mode 100644 index 0000000..befebd8 --- /dev/null +++ b/scripts/laser.gd @@ -0,0 +1,91 @@ +extends Node2D + +const BEAM_DURATION := 3.0 +const PRIMARY_TICK_DMG := 15 +const SPLASH_TICK_DMG := 3 +const TICK_INTERVAL := 0.5 + +@export var segment_size := 16.0 + +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 + +func _ready() -> void: + target = get_highest_hp_enemy() + if target == null: + queue_free() + return + spawn_beam() + +func _process(delta: float) -> void: + if done: + return + elapsed += delta + tick_timer += delta + if tick_timer >= TICK_INTERVAL: + tick_timer -= TICK_INTERVAL + do_damage_tick() + if elapsed >= BEAM_DURATION: + 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() + + place_seg("start", origin, angle) + + var n_mid := int(max(0.0, dist - segment_size * 2.0) / segment_size) + for i in range(n_mid): + var pos := origin + dir * (segment_size * (float(i) + 1.0)) + mid_segs.append(place_seg("middle", pos, angle)) + + place_seg("end", dest, angle) + +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 + +func do_damage_tick() -> void: + 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() -> Node: + var best: Node = 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/witch.gd b/scripts/witch.gd index 435d3fb..e6d73c3 100644 --- a/scripts/witch.gd +++ b/scripts/witch.gd @@ -8,6 +8,7 @@ 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") @@ -99,6 +100,12 @@ 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) func take_damage(amount: int) -> void: if is_invincible: From 85e0ca75ff4d95ae181ed2507702e326b454ccd8 Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 16:36:27 +0200 Subject: [PATCH 3/8] Polished Laser visuals and added perk that added retargeting on kill --- assets/music&sfx/sfx/laser.wav | Bin 0 -> 120296 bytes assets/music&sfx/sfx/laser.wav.import | 24 ++++++ scripts/laser.gd | 113 ++++++++++++++++++-------- scripts/perk_effects.gd | 18 ++++ scripts/witch.gd | 10 ++- 5 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 assets/music&sfx/sfx/laser.wav create mode 100644 assets/music&sfx/sfx/laser.wav.import diff --git a/assets/music&sfx/sfx/laser.wav b/assets/music&sfx/sfx/laser.wav new file mode 100644 index 0000000000000000000000000000000000000000..874f9a1e073f8bc0f998e8e97421f3a11273bcbe GIT binary patch literal 120296 zcmeI5Npj@Ka)$fuwA2xF6`4sg(^d;u%jZN6_~U>6$AA3s$GgYrbn5!#h-}&=$PqmzwIKS$S+; zChf|!Q<;^PxfAby`O9CvzC5jyc(r*tJU<`yPuuNgyWKre+3mJi)CJ3@3bU@Sd8lI|EKS8E{%^z{V@*eak{5&)pMJWZEv@BzLP~j+^P%tapc(&#x~BC<&wvD3a8kN)eY@Q%);gTZe5E zS@%Eu>H9xFE^K=_dtgkn`D_MEpu`L$CxxS(8XEt-m+q*I+5fc-jkNI)u@p?{LOHEXY=hGepQe^$FGx-gli7UEa)OqiE0$AJ z&6JwE>B{~%^cm_*-EZ4+X?fc(zy1F01)^e7tSLx}n=I%CZcs_8PFkgevXw!p~O5@ZEGM;4Zl^g^l4 z8?Nk)Bt1c$b^C2AqB&k8))3X#17eD&moHzK)E=O?l^O;VMUX~WiH9G)|9N5wXYojw z78LcNWn$+yH7H38l3GI~>29fMdoT2}f8Ei?Bu>4zyxq6oe|_C1$@ZB@AuiuPzdWP8 z$|Ar_Q5CU8iI9v{2BvM|-2d>yCz2DqUW#cd+bBf^VoFWkc%e5EeS$jU_S<#}#i{Lf z`26edU-xi-htFSLiPQn45Uoq!2X~i(stPIS)~lvuq+;vS_dh;bt}}nQyH{$OOkpGV zJkUYp*Bp|zZ2#AO_OCnolcx`_s@(gvh?YFI^<=gJg{_pEPNp=V+ z4%-cGtcVmzN(U1}rv_BLq$H_LF#YMr2gqr5fA=UcA>dn3+`xD;N(2o>z6?UApZV*K zZfonF``f|cR}$3*4b`(Gwa4u*?x{eE;&jQWQc)+IACX#nk3arA6)xoTFt@Dv#75Q^mrfs!&cj zzm>TE>25)Cx}RCrf-oWAn-VO-)L%XQtY3F@7t{7V#voHzbT?mq{k%=KFJE4tQc_Y< z>;e_2s)nkT)Hbp2e}1rCYjQtX+Dn8E6qm#0!S#XC0=}-bg6eGBYg=CXYq5lYfG3Bj ztA6^LxXbZFS@kUEaT4kG3{=wdvE!@KR0*iHKu;05I}NK1qAq6bd(2tPIqXU8ToLQt z6FEQfeeC~0hbYyODmcHjV_UO121XF8#d5hg@xQN`TOL2G0nc)_9F+1m?q;)necnF3 z9ME2&&7#)Ipj9fXBTXf?uqSgo$z9kB3}jgVSBeTYRn%Hqx7+S#vEsldE$7f+t-o7s zzZ}+w*WLE{83HqC(Vg#6HxkC06*zg$!vevCZO`Ty$3mzqpu&GeteeqK95t}{&T_g2 z)^D{-xQljQcdOTz^%K!L;NEJt!{|+x7H)^l!k|o6i5-ahr8k}8R@ZW73wueOu^1u7 zq^uL>ZnXIp9NhY?9#H zA^v5*`uwzcecHY}ZAn)2k5!e4%4nrxXJy?5%SV4Pbr*B7SOAl=fS}NM4lI&SzP>d+ z=dk6O&nY%>@NKvHWwZOd-oLC5`z>h-w^~d}`aY5qsY^P)#94{EU@`S)3zy{NSP1?Y z(?ZcnSzVjYnQeFB(~E5we%)@rtey|+7x=#Ie!tb2&{kYgSXU3QA?#TWqMkPwL9_VmY5Nwg(C?u zCf3tg7+uvmH+6BKia&4m&zqO+{vh?lVv>rIevhMxwOIKRVJ*bmu@FMomPAByvTXcx zEjMM2+xaj~v5=}rP#fI-nyB)U+K42aVFG}l*&2>?b{Y|Ls;h42_rIA>QsmHJ4WU9H zLB5aO9)*1f>YEsfKp+gzfwdy4$E=PZTgIFJ6`!f(aKv#FR|D=tECl9Ld*8&56cTfy8Xx92&IYrD$hF`%Ogo38nBKk&5$ zwN=6M)9N)r{C8MC?Kb-jGGSp-9A?_%hyE4IW?P?@jJE2eWG7ycxGu3nU-tmWr9#?m(>a4sV4ql_}E;N1Eg!vppb5&Ow= zKy!hk-D-`ArpVm5+HeIxD}uD57hRc#uURfKDO=;(AtKI4@qpQK_5{;=tSJqqp3y84 znlYyBsor`D3;axAk|~&iIa#h^q%AOQ}~puw=s0JAzdADB1ttV z`T#5903+Ml9_nDn83Bw{Z*gh;RX6mQonDXCOf zj7U`jqVlcK_Qbt2o7s<2Oyu-vx=$vdVO7-Lxc!gNHXi2Hj(sl{+ikQb|F=svkd(=Z zD9Ki;2`Gee4T=zf71$4AvRF=Rgb_1@5tu-tz1(1pQh!^wKf^FoO3%Xn&qZ=!gcR%5 zE`hF)6-IhLMntHnm1^eyPNQ<%z{ap#@wi+}?I|YRTQf{@OvAlK(Lm)+?M*wt5v6uL z!%9dh_WeB))>12#MSmTes(M+)w&&Xc8mEZ`MBqeDPjXritPW)WQ@20G88|dJWf&s< z3->V`A9=r3c|gIsYUTkebroBI>tOgarXr+LVoo$`t_7s_mL1@T29`YcE@-O!*fj%+ zmErx2yQ-!RutHXm6{3Y&%*f@26mudIV)t#gtG{4@G%@UvvMLL*Cwk~)imKAktv6iO#@()V%Kk03hmo6j zp}P4+GewdzV!%AAW}qTFbX{nf=2NO1 zDkUhTss^YeCt=TyIg!ojac<*sSsQn|T5EInIKYunMf9}F_bV}*tdimS$>vIN5>rZw zp!5VlGBpC{Ltt3N3e$&sJjLl|=JrSRu=4UqxQCTk0H;0RzGpkT^ofMR-sYJXH|M^1$-X4Jz16Y17@6%!~(fSkP?Q{S8&C2vg+L0 zD4zfP2E5!A0(7eT0i&W3Fu1)^Pk>ES#EE$oP%)=a1QW-exk!ekIAys;E+?hS+3ZSJ ziukrFq>&Yy`UfE-tvss8iCsT*gQ@Mz+_`NdGmLFh(h<%LgR1yfKFFFYnd-`yb>h%V zyeKFPf{}N^i2g`WX=R`aMJ$5A4e`YU%!$%jAnk~e&H`9^!n(3?ZtL=VTjj+;vJanD ztD`6OMyG{ttw>lX;N%jgnJs4S!gkWMBb*mTA!1IAVBMBYZs*hF#^wvUt@2`jwGa14 zH^J0A@t{rJP_Y9n<%EP5oYex7N|TL{qA2l5z8bTTl>H&(kW{_Vs7O^9tt3YQZqu$n z)Aw;;u~O|*v_x7>Bf-GC}2RMDOvgUA6(I!@;&!f9BM zh$tXV-ppNE0$-malPyt73sXMtpHde)4{*#8DmA)7KDW=cR5Vm3@`6CB$+4Fr&A22J z#JnR!1K9Npr_iX-U5kM*>fxfCuagB8E6hG{V=p95OPA6uWjUALOd_AmC7;2JmpEmL z3J*clxLP^2D!Z3*sps7bLh1EvO5}q~OOzHyCi2N_a0^S9Fxqvlk)qP0)iJ=re6)y( z2X2HuLaipy;dDznhtDPHT*pE;b5%kWBdiGZFc?B5N0_bGcsSyQfgicZv?SBQNHyg% zE%l{!Gq$+#50auH_m|#I(db%kokB12$lb~0Wk<<-h!LkTD^w~O(~bY`Z|vh$pbF_R z7DVN7GOS}n`LtMX>^FY1A2&rs6Y4Qvky6D!?47>wU`c%CLZl zL!STz@p_BVfHZnD^xXxaLUuXIRYnb!tVV-f*8)-GA+22K`(ne78Qoy%3d^G$fG!ft zffBMQjR}>m3rSubcU6h+S+3)*p)ePKKuM+lBSb1&*)4q3kNz3_MCk^jVRYI*1JS6b zkNyWT{{HPPAY@w=VbErAYy~}`=V!n20kD2x&!a19s{HKI)9+$|33Gw#`2t^vwZymh z;QRl047DDcds|~Z`*x&tJ>H2u3=fMu-1F|jO%iXSb&{-ORI3>C*;e?We!5 z3WGFDjxX@7!UA7XOB1pxW;r&VLeg}{K}Ncr`6L-wFA|~WVJx4w+63EZwTdrCS1qyI$Fi?imDV5FUhJMr!qY0I^p;>#N;_1zDjuF%omt|kFQgwyqd<8 z;gOU=>wpu6^SQq7?*a_(+eE7*T1Ur{lGxf|ic=;f3&>gK*$|WKcoWN-;#+E$YRWLp z@5YZ&(eedK$3OOq9p;CE=Gwsj;Y*x1kQ8*4kfe@ab&ymEAhjy6ASNFP=7c-37jw$u z1E+VK(gJkOj_6``?tY=Pt%Co<+yMALceR#H6-layDj{99D;21aQqyrggb{8bnXqb? z&WfnJ%`SIjO2=G~)7L`8f8M6FsWMurAy6TVa6E8AM&UxzXLfpLkkaK$-2F->lZs`5 z_z${r1K;09$vWJWdOr>U+er$07gU9uWwz%cF!vk;5lc$WozIgnRD;yrWS2X#0!a00 z-Bg8MuniHYBl=S@I=5CjMsM%^3aF5?jKV$QG)<@88~H4FB6nw2mostaD^){iwuM4< zND56A1?mX}>VyiFNr@Y(w*Jpl6~aA-JfH9_q)4zRKa8dF_)tEJb}15wXJ><5@W5sS zn(jqSRfv(zXsTkqr{dU8-2iJ?giSVC?R=nefT8WX0-WIaBq#J4F(;nStvS_;3LQ&t zpft65r~PpE8~W5$;d|UoQR@9zVI5o`p+e3w?lp2mPQvqOD!0_r5uMxWFfaU>3}WJePGU4wX(z=X4HX3uK7t70 zxyt3)G?km0oUAIC=E^GuGU|CwV>DHDsM_6DMdb956$amL1Wu%#aQs{wgEDNN^nC`)&X|nViz> z6)7yz7~vu&T-ATX54g2%U?2@v^2r6FwK7pr?>hbDszR_HIf3T}g8d%yjTm~$a2zdt zK>L^e%5_l6x2f@e&=pW^v#upQSg8h8#t9=WIUAPFvSj8P`S1cNw|j@R#%&d&Iv!V6 zg(~HQ$6P)#B+SK<^TJRgrRgX=mHtt^zm*+~-|mo*)9}dFD%u2vA%F&|%>6MZjGXa^ zleKi`$lrr}BS_+-^aK#^C@)O@&#Ik$2izN(t+=70z#`cMG^&>Ftklpl6$46M3Kcl< zNDR%1@+@VX-b?)ErAkNE8mQ1wNRvf~ljoDy zL+Y|DJxn1V%%IF}!A!UC8JeI{^F(;0Ot7@Y!nG*bl1^QgCR;LzHHUQzR(K1aLF7@D z#D8k)0dA4e^+akCtR~8g1&7C8ueKWV#tig#kVEAc?y3+R(qJaEPUy{bm6`xZriY>A zJ)Fkps2b?Aji8_BZiukXvSPVIkTSQO&*mBD(G=NzUCu) zkd>t_#^E`fHe}kIxIJ`ZExw>4*5R^ zNfDMlvNqbt+Q__ADnQN$XjWJM+~Wu6Vu7r=Lu3+!>yUC@sD~(Oq&uFY)E4#dpStZ` zACs|cJYX59H@Mhu+~s$?g=4aZ8`RC`ucBxbCQ+2&2a^u*lUDSmklUc)imi^kyj0<(1PE=7>Hsv{ zhD=zLya(y9kikcY*>M)YY>p$)+`^o!++WBgkvbs{j&(g6Z0>nc;wM2A2Pr4uU?y4B z5JCNJfh?3;&f|1gsD%8+d-{fMb4GW5mgCLAQsgec%nJl%wq#CdC6X{qz$qkHM5$2K z6KJqiK|-sRi3u5egfaI%oqo!Sy5ZTVjOhZ9po0JN{1r(Gx>EixCn>_# zv!t9{m0OO)grD^5?nzPj$*LQ4dH{4WJjR6t{2yGO8g%XjD<8UINx=mUE-6LIq2|dE z@O{)gxpT{&>)H2)gjid-hT)oT44YkPG4KD}%BD&>!yz;UPC!I$6a{?lLUQ8SjmB)@ z8)%ZQM5OYvkd&Io!IL?X2;PsPKXQSErm8<<(+?q=DohcOp}Lnw6*5{k zql$i#;uN*g^Rc3ir=VV>8ER^ko7~T6swiIfSAHA??EU!0%5;G}P;o*{)i{yoJLbf* zPR{}BCT-sFL*=_F+#sp)ehjS>Ey|yc+;SX5n4Hcq^}eGK-o#-?O_i(esY<g12AUEbt`0_5}}fh$&0AC$F?b{dQNF0)%bULPdnCOXtE8c^k4>l2EwTF z4zO`=(lMuOD!0@Sg!Eyg)Wr5J_c;y#i>M%|CJ}|=gvVaoK6!$pa&mKN6ZM`XV*V~I zP2P2PN@GQ74d_WdW@3BAu%Zbn`o0{>sfo&^A8$&*2Rr= z-3uPyR^?DhUPVStzrKse063Ah=97klCEMzD_Qpz~xmsBc}6Q5|@ zTwwtd72N^rTVU#5kAl4KGln%uzvt;}SUl~X5D}X~RdMNYbvp|bP&M$KcoazsP3xR0iqv6j^vd6x%8ZP z=}NC>`nr))bCY-7vxrLF03)b~_yI9XN$Uv{xg8lpq^_^)mB-^+T7a^f@+>&+R)=W3 zF!E(k;ieeX4w!+@UJvOJxQS|csd4vy7AQf5mMRGSqcIow*2FB&5~J#O-SOzw7C`8I ztg6^gr|`((7SuplE+i$B#f*8DT3dOL1B$4~3FeEDX^CKsj0B}QnM+st zydU_og4BaZoU+L|l>HBt$M15hczq5UY&PoaqJKD9Tg6YSsG>XIkRMhBAuScEG@Z*( z3%S{+oEZjVK6&7i8k^haf&W>Ly(BAs1f_;9qtl%F&Hcy^1N2vL)rN-OsbCHkC553B z7L><=UTDB)e(nhD{gDQ|m;8A-PBz3`a>ArFPB)B9RiRHw7)z^8Ov(-tlx##eiWGs8 zoa8sD>p4E-L?5uDQRX!BohKIp6N=2YUf?8?u2b&yp$aFK;|7u{sVMpN+-mZbNeZI+ z;IiT=0W_Z)Gpot}F)B*7oM4C>GY)M zWj*Z$u%t8<|2|%kf5UXi0A}?#+!ANPP)?mPem2iv4eeuc&foaC3?a~wgFdv2!;Yu7WJEp4jE^}!kP%OKv5zczyNa;pm9ZiUks z40--jmNiwRDrhOyRr|<~ZlCA|fc>4W)Yqu1sd13gg+z6JBVObRrA?Khl%i_cOi{k) zd=P=RbnBE&W;`3Gix}-X7cn~krT0VK2UQg(W2T(u)KgT~G1&F4nu{tlE5rA-iz$c` zeQ89lGS#GYrJP?=Sl6@94PDtll|~g>RkGKwaYI$X$((sglZ2JLOjt}zZ@MAl+}1}a z3jp(|nl)6VoP^OjX+)ZcBt|po#?s6J>63cx{OHwas~Ul-;zU^Fv_=M#y~Sr65j6vp z?Nt`5w?kFVNg*=mq}Z~Hk4r5qU_7T+x4&9dND4^7`iWA(iQA-fnlNL-hZ=>{f=ppo z#OnF+FYE`&0X9G-2hJSDdpzwlog=KIRJiWAqO}Eb533kcQ&m-P0vm%9KF=tn)=

FD)Z8ok@@4N#GuRPG5fDKud^lt`lp5K9BTamIE>t9LrA0u==k@_}ypR9C4==fjdS>ql6f ztlsIM3RDu1>GOD52NJ|-q}0ja-47~5g_fu!j!+HMsHX15?sud{l?q5k47$UeR&$qx zTrJf!>Zza2J05+6N~cm{RP@+UqEZ=Rl-CRR=2%a)?Bgqr^aD6ICuK<~7LG$CI?M0HuL3 z#iC^cgnF^CnO2Ntw7_VADhntF$ZZp~Pr9fioB5>BXL^_AgTWjhvv8MvR#p8DA7nk< z>lP;1V1@jK7MjLLOU;U&NT+c(d*wVMl)NWjXsl=XXu=OdrtoVXanj}@tMhcz%BmTM z6MM-FOB7e;OX-+gHKg#PJ{`-6$}pEwlM+{o?S$pqZN}voQFBfodc+z(HH6K$kuX|{ zQc1NVQNK6%u$YcHNvFr2FLi%#1s_#Ph8NWSPr*ucC7Y^LP3#WqZQo{VCZ;?o0Li?Z zS~5QDhh8-NUrLD=icM7;6m|PnHBafd+zi%-MsxhjOH{{_q5x2~Qm{D>W#Xz1K*Mb+ zQDY&D$a5O~-7zP;*Z$9FrC3zCrm7)^le=;AgwyAp9^PxeHzF!Qq*fWxL>`ub>Lh_? zJ20nXNc3mH0Yg?*f7p+`rLw7#sFe3>#-eU7MpVK{2N9saus$}8+TR|3)U2kwq_8%N zD(VO{+tz?n?S~vUk_CW>`<2KM<%&kt3`X@{<@dO_l%b~)MFR^|ND7u5B$b9w`t^*+IHZD zVNs2o2F7Eaj^uOHNU67V(kb0Gaxx7&{Bk|2=3cOp(-@Tx1+83Bw3d|93{^!?PA3`a zV~*#+Ely==h-&0?p+BQaP==~%q;-PRD5?Q5_CYmr8W68H(6{~ig zbk$K$r)}SIk2+Ko#_J~}v?Rx%@w)26MoKLSG~T`1<~d3Rw3;IzC5Wx@wZ|P>S)j)7 zETT+RYR@{q$CO4?tpGINxeit80MnhSIVm-boSK`y?Vc%xk_FW&RrEWV2awkrDYdnI z%l%D*A1|K>AoT8O12w{GY5aD(YkQZyHjopmX_Qnun|Iv143S=U8kKZ>tfo;?9SJl( zOk=5jtqdhbPK~WR$d1*Bu%(g`ZBk9tsHQprBOAw#<Vu!aM1`b>EodFOVtjbjN} zDM}-%(*_=H4-*FqDXoCRDCTNX6H^*d4F}@%nNk|k{tO|l^v1Gnr%yfZF{}k7jbj$L z2JzxTiyEF%)36_5JX 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: From 89f0faea9a1de559db10dd0832252fae8b81c6b0 Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 16:56:41 +0200 Subject: [PATCH 4/8] Rewritten some recipes. Added Chilli fruit to give laser spell a recipe --- scenes/chili.tscn | 22 ++++++++++++++++++++++ scenes/fire_slime.tscn | 8 ++++++++ scripts/SpellLibrary.gd | 24 ++++++++++++------------ scripts/cauldron_bar.gd | 3 +++ scripts/chili.gd | 2 ++ scripts/chili.gd.uid | 1 + 6 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 scenes/chili.tscn create mode 100644 scripts/chili.gd create mode 100644 scripts/chili.gd.uid 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/scripts/SpellLibrary.gd b/scripts/SpellLibrary.gd index 24aac26..c611c9d 100644 --- a/scripts/SpellLibrary.gd +++ b/scripts/SpellLibrary.gd @@ -2,6 +2,7 @@ extends Node const APPLE = 0 const GRAPE = 1 +const CHILI = 2 const NONE = "NONE" const SHURIKEN = "SHURIKEN" @@ -10,23 +11,22 @@ 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], - LASER: [], + 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/cauldron_bar.gd b/scripts/cauldron_bar.gd index 7965f73..1817727 100644 --- a/scripts/cauldron_bar.gd +++ b/scripts/cauldron_bar.gd @@ -87,6 +87,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,6 +119,7 @@ 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 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 From 7d17c1312e5703dbfa5b18a75a2dfbdb39b12445 Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 17:01:57 +0200 Subject: [PATCH 5/8] Added Elite enemies before wave switch --- data/spawn_stages.json | 38 ++++++++++++++++++++------------------ scripts/SpawnStage.gd | 3 ++- scripts/spawn_control.gd | 32 +++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 22 deletions(-) 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/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/spawn_control.gd b/scripts/spawn_control.gd index 2f0330b..762c687 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 := 10 +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: From ec27fa03e4bc96cb79cfdd603b2170e749199ff1 Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 17:25:08 +0200 Subject: [PATCH 6/8] added fruit queue up to three fruits while brewing is busy --- scripts/DropsBase.gd | 12 +++++++++--- scripts/cauldron_bar.gd | 4 +++- scripts/debug_menu.gd | 16 ++++++++++++++++ scripts/player.gd | 24 ++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) 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/cauldron_bar.gd b/scripts/cauldron_bar.gd index 1817727..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 @@ -122,7 +123,8 @@ func brew(fruits): 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/debug_menu.gd b/scripts/debug_menu.gd index 6d44c4a..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 @@ -73,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): @@ -100,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/player.gd b/scripts/player.gd index d621ab6..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("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(): From 76e36c7c9c373a0fef6be674beb738c27f307b0e Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 17:46:33 +0200 Subject: [PATCH 7/8] Changes several hp and damage numbers to get slightly better balance --- scripts/blue_slime.gd | 1 + scripts/fire_slime.gd | 1 + scripts/perk_effects.gd | 36 +++++++++++++++++++++++++----------- scripts/spawn_control.gd | 2 +- scripts/witch.gd | 14 ++++++++------ 5 files changed, 36 insertions(+), 18 deletions(-) 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/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/perk_effects.gd b/scripts/perk_effects.gd index ccd2584..caa8584 100644 --- a/scripts/perk_effects.gd +++ b/scripts/perk_effects.gd @@ -16,6 +16,7 @@ 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 @@ -68,8 +69,8 @@ func _ready() -> void: 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 @@ -84,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" @@ -154,11 +164,14 @@ 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 @@ -255,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/spawn_control.gd b/scripts/spawn_control.gd index 762c687..5101aa3 100644 --- a/scripts/spawn_control.gd +++ b/scripts/spawn_control.gd @@ -8,7 +8,7 @@ var elapsed_time: float = 0.0 const STAGES_JSON = "res://data/spawn_stages.json" -const ELITE_HP_MULT := 10 +const ELITE_HP_MULT := 20 const ELITE_SPD_MULT := 0.25 const ELITE_SCALE := 3.0 diff --git a/scripts/witch.gd b/scripts/witch.gd index 1b5be8a..92b57d6 100644 --- a/scripts/witch.gd +++ b/scripts/witch.gd @@ -13,9 +13,10 @@ var shuriken_count = 1 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 @@ -53,9 +54,10 @@ func _on_collect(DropsBase): func shoot_fireballs(): var enemies = get_tree().get_nodes_in_group("enemies") - for enemy in enemies: - if not is_instance_valid(enemy): - continue + 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: var dir = global_position.direction_to(enemy.global_position) _face_direction(dir) var fb = fireball.instantiate() @@ -126,7 +128,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: From 8543199974c1136e3ecd196c29bbf1ee9cf02cec Mon Sep 17 00:00:00 2001 From: Artur <2123806@stud.th-mannheim.de> Date: Tue, 2 Jun 2026 17:48:10 +0200 Subject: [PATCH 8/8] fix fireball bug on spam --- scripts/witch.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/witch.gd b/scripts/witch.gd index 92b57d6..891ade4 100644 --- a/scripts/witch.gd +++ b/scripts/witch.gd @@ -58,6 +58,8 @@ func shoot_fireballs(): 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) _face_direction(dir) var fb = fireball.instantiate()