Compare commits

...

10 Commits

Author SHA1 Message Date
Artur David 2c3d8c7cfd added purge sounds 2026-06-24 00:06:45 +02:00
Artur David 35772ba2a6 hide Fireball Spread once a cast archetype is chosen, it did nothing there 2026-06-23 23:35:40 +02:00
Artur David d5aca6bdf4 weaken tornado spawned by fireball perk to 33% damage 2026-06-23 23:33:04 +02:00
Artur David dd4fcbdf2c stop hurt i-frames from blocking damage, only block while dying 2026-06-23 23:28:01 +02:00
Artur David 960a8612b4 fix perk pool losing sync with available_perks after archetype pick 2026-06-23 08:58:48 +02:00
Artur David 882d44264a add archetype-gated follow-up perks for Shotgun and Minigun
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 08:52:50 +02:00
Artur David ac86c4371e add Fireball Shotgun and Minigun cast archetypes 2026-06-23 08:49:20 +02:00
Artur David 0ad1ce1ca0 rework AOE Fireball as an impact archetype, add fireball-spawns-tornado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 08:46:22 +02:00
Artur David 83c8a61f14 filter perk offers by mode and enforce archetype exclusivity 2026-06-23 08:40:42 +02:00
Artur David 6d1fcfce07 add archetype slot/mode fields to perks 2026-06-23 08:36:20 +02:00
14 changed files with 279 additions and 22 deletions

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bdqk0x4qyk3mc"
path="res://.godot/imported/pruge_bass.wav-c5ceaea0b44c81c9961ac70c3dc5f07c.sample"
[deps]
source_file="res://assets/music&sfx/sfx/pruge_bass.wav"
dest_files=["res://.godot/imported/pruge_bass.wav-c5ceaea0b44c81c9961ac70c3dc5f07c.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bqg3q37ugk8ja"
path="res://.godot/imported/purge_build_up.wav-478e240cc516d2308f089ccc744223fa.sample"
[deps]
source_file="res://assets/music&sfx/sfx/purge_build_up.wav"
dest_files=["res://.godot/imported/purge_build_up.wav-478e240cc516d2308f089ccc744223fa.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bci64umwgnpjj"
path="res://.godot/imported/purge_impact_boom.wav-dc17074eb0200fae32cf877c94544e56.sample"
[deps]
source_file="res://assets/music&sfx/sfx/purge_impact_boom.wav"
dest_files=["res://.godot/imported/purge_impact_boom.wav-dc17074eb0200fae32cf877c94544e56.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

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://xkygjr5ewvic"
path="res://.godot/imported/purge_sustained_boom.wav-ebb185be11c79583807c007ee85b5815.sample"
[deps]
source_file="res://assets/music&sfx/sfx/purge_sustained_boom.wav"
dest_files=["res://.godot/imported/purge_sustained_boom.wav-ebb185be11c79583807c007ee85b5815.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

View File

@ -6,4 +6,6 @@ class_name Perk
@export var stats: String = ""
@export var spell: String = SpellLibrary.NONE
@export var icon: Texture2D = null
@export var archetype_slot: String = ""
@export var requires_mode: String = ""
var effect: Callable

View File

@ -62,7 +62,7 @@ func _play_hit_sound() -> void:
ap.play()
func take_damage(amount: int) -> void:
if is_dying or is_hurt:
if is_dying:
return
hp -= amount
if hp <= 0:

View File

@ -1,7 +1,7 @@
extends ProjectileBase
var fireball_aoe = false
var explosion_scene = preload("res://scenes/explosion.tscn")
var tornado_scene = preload("res://scenes/tornado.tscn")
@onready var perk_effects = get_node("/root/Game/PerkEffects")
func _ready() -> void:
@ -9,12 +9,24 @@ func _ready() -> void:
damage = 10
func _on_body_entered(body: Node2D) -> void:
fireball_aoe = perk_effects.fireball_aoe_enabled
if body.is_in_group("enemies"):
body.take_damage(damage)
if fireball_aoe:
var boom = explosion_scene.instantiate()
boom.global_position = global_position
get_parent().call_deferred("add_child", boom)
queue_free()
if not body.is_in_group("enemies"):
return
body.take_damage(damage)
match perk_effects.get_mode("fireball.impact"):
"explosion":
_spawn_explosion()
"spawn_tornado":
_spawn_tornado()
queue_free()
func _spawn_explosion() -> void:
var boom = explosion_scene.instantiate()
boom.global_position = global_position
get_parent().call_deferred("add_child", boom)
func _spawn_tornado() -> void:
var tw = tornado_scene.instantiate()
tw.global_position = global_position
tw.explosion_damage = int(round(tw.explosion_damage * 0.33))
get_parent().call_deferred("add_child", tw)

View File

@ -28,15 +28,22 @@ func _ready() -> void:
func _process(delta: float) -> void:
pass
func _is_eligible(perk: Perk) -> bool:
if perk.requires_mode == "":
return true
var parts = perk.requires_mode.split(":")
return perk_effects.get_mode(parts[0]) == parts[1]
func show_perks():
player.stream = level_sound
player.play()
if perks.is_empty():
var eligible = perks.filter(_is_eligible)
if eligible.is_empty():
return
var tween = create_tween()
tween.tween_property($ColorRect, "color:a", 0.6, 0.3)
get_tree().paused = true
var shuffled = perks.duplicate()
var shuffled = eligible.duplicate()
shuffled.shuffle()
var chosen = shuffled.slice(0, 3)
await get_tree().create_timer(0.4).timeout
@ -54,6 +61,9 @@ func select_perk(perk):
_spawn_level_up_particles(get_node("/root/Game/World/Witch").global_position)
_spawn_level_up_particles(get_node("/root/Game/World/Player").global_position)
perks.erase(perk)
if perk.archetype_slot != "":
for p in perks.filter(func(p): return p.archetype_slot == perk.archetype_slot):
perks.erase(p)
for child in $HBoxContainer.get_children():
child.animate_out()
var tween = create_tween()

View File

@ -6,7 +6,7 @@ var fireball = preload("res://scenes/fireball.tscn")
var throwing_knife = preload("res://scenes/throwing_knive.tscn")
var cauldron
var available_perks: Array[Perk] = []
var fireball_aoe_enabled = false
var spell_modes: Dictionary = {}
var laser_retarget_enabled = false
var throwing_knife_enabled = false
var throwing_knife_cooldown: float = 2.0
@ -39,6 +39,12 @@ func _stat_new(label: String, val: String) -> String:
func _stat_toggle(label: String) -> String:
return "[color=#aaaaaa]" + label + "[/color]\n[color=#888888]OFF[/color] → [color=#77dd77]ON[/color]"
func get_mode(slot: String) -> String:
return spell_modes.get(slot, "base")
func set_mode(slot: String, mode: String) -> void:
spell_modes[slot] = mode
func _ready() -> void:
cauldron = witch.get_node("CauldronBar")
@ -78,19 +84,71 @@ func _ready() -> void:
var faoe = Perk.new()
faoe.name = "AOE Fireball"
faoe.description = "Fireballs explode on impact"
faoe.description = "Fireballs explode on impact instead of hitting a single enemy"
faoe.stats = _stat_toggle("AOE")
faoe.spell = SpellLibrary.FIREBALL
faoe.icon = _icon_fireball
faoe.effect = fireball_aoe
faoe.archetype_slot = "fireball.impact"
faoe.effect = fireball_set_impact_explosion
available_perks.append(faoe)
var ftor = Perk.new()
ftor.name = "Fireball: Spawn Tornado"
ftor.description = "Fireballs spawn a tornado on impact instead of exploding"
ftor.stats = _stat_toggle("Tornado")
ftor.spell = SpellLibrary.FIREBALL
ftor.icon = _icon_fireball
ftor.archetype_slot = "fireball.impact"
ftor.effect = fireball_set_impact_tornado
available_perks.append(ftor)
var fsg = Perk.new()
fsg.name = "Fireball: Shotgun"
fsg.description = "Fireball fires a spread of pellets in front of you instead of homing shots"
fsg.stats = _stat_toggle("Shotgun")
fsg.spell = SpellLibrary.FIREBALL
fsg.icon = _icon_fireball
fsg.archetype_slot = "fireball.cast"
fsg.effect = fireball_set_cast_shotgun
available_perks.append(fsg)
var fmg = Perk.new()
fmg.name = "Fireball: Minigun"
fmg.description = "Fireball rapid-fires at the nearest enemy instead of one burst"
fmg.stats = _stat_toggle("Minigun")
fmg.spell = SpellLibrary.FIREBALL
fmg.icon = _icon_fireball
fmg.archetype_slot = "fireball.cast"
fmg.effect = fireball_set_cast_minigun
available_perks.append(fmg)
var smp = Perk.new()
smp.name = "Shotgun: More Pellets"
smp.description = "Fire more pellets per shotgun blast"
smp.stats = _stat("Pellets", str(witch.fireball_pellet_count), str(witch.fireball_pellet_count + 2))
smp.spell = SpellLibrary.FIREBALL
smp.icon = _icon_fireball
smp.requires_mode = "fireball.cast:shotgun"
smp.effect = shotgun_more_pellets
available_perks.append(smp)
var msu = Perk.new()
msu.name = "Minigun: Spin-Up"
msu.description = "Fire fireballs faster in minigun mode"
msu.stats = _stat("Interval", "%.3fs" % witch.fireball_minigun_interval, "%.3fs" % maxf(0.03, witch.fireball_minigun_interval - 0.015))
msu.spell = SpellLibrary.FIREBALL
msu.icon = _icon_fireball
msu.requires_mode = "fireball.cast:minigun"
msu.effect = minigun_spin_up
available_perks.append(msu)
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.requires_mode = "fireball.cast:base"
fsp.effect = fireball_spread
available_perks.append(fsp)
@ -166,8 +224,17 @@ func laser_retarget():
func double_shuriken():
witch.shuriken_count += 2
func fireball_aoe():
fireball_aoe_enabled = true
func fireball_set_impact_explosion() -> void:
set_mode("fireball.impact", "explosion")
func fireball_set_impact_tornado() -> void:
set_mode("fireball.impact", "spawn_tornado")
func fireball_set_cast_shotgun() -> void:
set_mode("fireball.cast", "shotgun")
func fireball_set_cast_minigun() -> void:
set_mode("fireball.cast", "minigun")
func fireball_spread():
witch.fireball_max_targets = witch.fireball_max_targets * 1.5
@ -177,9 +244,35 @@ func fireball_spread():
fsp.stats = _stat("Targets", str(witch.fireball_max_targets), str(witch.fireball_max_targets * 1.5))
fsp.spell = SpellLibrary.FIREBALL
fsp.icon = _icon_fireball
fsp.requires_mode = "fireball.cast:base"
fsp.effect = fireball_spread
available_perks.append(fsp)
func shotgun_more_pellets() -> void:
witch.fireball_pellet_count += 2
var smp = Perk.new()
smp.name = "Shotgun: More Pellets"
smp.description = "Fire more pellets per shotgun blast"
smp.stats = _stat("Pellets", str(witch.fireball_pellet_count), str(witch.fireball_pellet_count + 2))
smp.spell = SpellLibrary.FIREBALL
smp.icon = _icon_fireball
smp.requires_mode = "fireball.cast:shotgun"
smp.effect = shotgun_more_pellets
available_perks.append(smp)
func minigun_spin_up() -> void:
witch.fireball_minigun_interval = maxf(0.03, witch.fireball_minigun_interval - 0.015)
if witch.fireball_minigun_interval > 0.03:
var msu = Perk.new()
msu.name = "Minigun: Spin-Up"
msu.description = "Fire fireballs faster in minigun mode"
msu.stats = _stat("Interval", "%.3fs" % (witch.fireball_minigun_interval + 0.015), "%.3fs" % witch.fireball_minigun_interval)
msu.spell = SpellLibrary.FIREBALL
msu.icon = _icon_fireball
msu.requires_mode = "fireball.cast:minigun"
msu.effect = minigun_spin_up
available_perks.append(msu)
func brew_explosion():
cauldron.brew_explosion = true

View File

@ -4,6 +4,7 @@ signal health_changed(current_hp: int, max_hp: int)
signal died(attacker)
var camera
@onready var perk_effects = get_node("/root/Game/PerkEffects")
var is_casting = false
var fireball = preload("res://scenes/fireball.tscn")
var shuriken = preload("res://scenes/shuriken.tscn")
@ -19,6 +20,9 @@ var max_hp: int = 50
var current_hp: int = 50
var is_invincible: bool = false
var fireball_max_targets: int = 5
var fireball_pellet_count: int = 6
var fireball_minigun_shots: int = 6
var fireball_minigun_interval: float = 0.08
const HP_BAR_WIDTH = 20
const HP_BAR_HEIGHT = 3
@ -55,6 +59,15 @@ func _on_collect(DropsBase):
func shoot_fireballs():
match perk_effects.get_mode("fireball.cast"):
"shotgun":
await _cast_fireball_shotgun()
"minigun":
await _cast_fireball_minigun()
_:
await _cast_fireball_base()
func _cast_fireball_base() -> void:
var enemies = get_tree().get_nodes_in_group("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))
@ -64,15 +77,46 @@ func shoot_fireballs():
continue
var dir = global_position.direction_to(enemy.global_position)
_face_direction(dir)
var fb = fireball.instantiate()
fb.global_position = global_position
get_parent().add_child(fb)
fb.launch(enemy.global_position)
_spawn_fireball_toward(enemy.global_position)
_play_fire_sfx()
camera.shake(0.15, 0.5)
await get_tree().create_timer(0.12).timeout
$AnimatedSprite2D.play("south")
func _cast_fireball_shotgun() -> void:
var target = get_nearest_enemy(global_position)
var aim_pos = target.global_position if target != null else global_position + Vector2.DOWN * 100.0
var base_dir = global_position.direction_to(aim_pos)
_face_direction(base_dir)
var spread_deg = 40.0
for i in range(fireball_pellet_count):
var t = float(i) / float(max(1, fireball_pellet_count - 1))
var angle_offset = deg_to_rad(lerp(-spread_deg / 2.0, spread_deg / 2.0, t))
var dir = base_dir.rotated(angle_offset)
_spawn_fireball_toward(global_position + dir * 300.0)
_play_fire_sfx()
camera.shake(0.2, 0.6)
$AnimatedSprite2D.play("south")
func _cast_fireball_minigun() -> void:
for i in range(fireball_minigun_shots):
var target = get_nearest_enemy(global_position)
if target == null:
break
var dir = global_position.direction_to(target.global_position)
_face_direction(dir)
_spawn_fireball_toward(target.global_position)
_play_fire_sfx()
camera.shake(0.08, 0.3)
await get_tree().create_timer(fireball_minigun_interval).timeout
$AnimatedSprite2D.play("south")
func _spawn_fireball_toward(target_pos: Vector2) -> void:
var fb = fireball.instantiate()
fb.global_position = global_position
get_parent().add_child(fb)
fb.launch(target_pos)
func _face_direction(dir: Vector2) -> void:
if abs(dir.x) >= abs(dir.y):
$AnimatedSprite2D.play("east" if dir.x > 0 else "west")