Compare commits

...

5 Commits

Author SHA1 Message Date
Artur David 31b368bc65 spiral swirl: fire swirl rotates outward as a pinwheel covering a wider area
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NTjSa2VgCtWZWVm573NB6
2026-07-07 14:00:46 +02:00
Artur David 1c54f7f2db chain kill: laser spawns a shuriken at every enemy it kills
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NTjSa2VgCtWZWVm573NB6
2026-07-07 13:59:58 +02:00
Artur David a2646d9690 reaper's pull: tornado collects nearby fruits continuously while active
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NTjSa2VgCtWZWVm573NB6
2026-07-07 13:59:22 +02:00
Artur David 4ce2603d40 repulsion wave: tornado cast blasts all enemies away from the witch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NTjSa2VgCtWZWVm573NB6
2026-07-07 13:58:39 +02:00
Artur David 7743a9b8d2 marked prey: shuriken tags enemies yellow, next spell hit triggers explosion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NTjSa2VgCtWZWVm573NB6
2026-07-07 13:57:59 +02:00
7 changed files with 153 additions and 2 deletions

View File

@ -20,6 +20,12 @@ var death_sound = preload("res://assets/music&sfx/sfx/hit2.wav")
var last_direction := Vector2.DOWN
signal died
var xp = 1
var explosion_scene = preload("res://scenes/explosion.tscn")
var is_tagged: bool = false
var _push_velocity: Vector2 = Vector2.ZERO
func apply_push(dir: Vector2, strength: float) -> void:
_push_velocity = dir * strength
func _ready() -> void:
add_to_group("enemies")
@ -41,6 +47,8 @@ func _on_base_body_exited(body: Node2D) -> void:
func die():
is_dying = true
if is_tagged:
modulate = Color.WHITE
collision_layer = 0
var death_anim: String
if abs(last_direction.x) >= abs(last_direction.y):
@ -66,6 +74,12 @@ func _play_hit_sound() -> void:
func take_damage(amount: int) -> void:
if is_dying:
return
if is_tagged:
is_tagged = false
modulate = Color.WHITE
var boom = explosion_scene.instantiate()
boom.global_position = global_position
get_parent().call_deferred("add_child", boom)
hp -= amount
if hp <= 0:
die()
@ -99,6 +113,11 @@ func _chase_witch() -> void:
if direction != Vector2.ZERO:
last_direction = direction
velocity = direction * speed
if _push_velocity != Vector2.ZERO:
velocity += _push_velocity
_push_velocity *= 0.82
if _push_velocity.length() < 5.0:
_push_velocity = Vector2.ZERO
if direction == Vector2.ZERO:
animated_sprite_2d.play("idle")

View File

@ -1,6 +1,9 @@
extends ProjectileBase
var _is_spread_child := false
var _spiral_rotation_rate: float = 0.0
var _lifetime: float = 0.0
var _max_lifetime: float = 0.0
const DIRS := [
Vector2(1, 0),
@ -17,11 +20,29 @@ func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("enemies"):
body.take_damage(damage)
func _process(delta: float) -> void:
if _max_lifetime > 0.0:
_lifetime += delta
if _lifetime >= _max_lifetime:
queue_free()
return
if _spiral_rotation_rate != 0.0:
direction = direction.rotated(_spiral_rotation_rate * delta)
super(delta)
func _ready() -> void:
damage = 6
if _is_spread_child:
super()
return
var perk_effects = get_node("/root/Game/PerkEffects")
if perk_effects.get_mode("fire_swirl.cast") == "spiral":
_spawn_spiral()
else:
_spawn_burst()
queue_free()
func _spawn_burst() -> void:
var spawn_pos = global_position
for dir in DIRS:
var bullet = duplicate()
@ -29,4 +50,15 @@ func _ready() -> void:
get_parent().add_child(bullet)
bullet.global_position = spawn_pos
bullet.launch(spawn_pos + dir * 1000)
queue_free()
func _spawn_spiral() -> void:
var spawn_pos = global_position
for i in range(8):
var angle = TAU * float(i) / 8.0
var bullet = duplicate()
bullet._is_spread_child = true
bullet._spiral_rotation_rate = 2.0
bullet._max_lifetime = 3.0
get_parent().add_child(bullet)
bullet.global_position = spawn_pos
bullet.launch(spawn_pos + Vector2.from_angle(angle) * 1000)

View File

@ -14,6 +14,7 @@ const FADE_OUT := 0.5
@onready var perk_effects = get_node("/root/Game/PerkEffects")
var beam_seg := preload("res://scenes/beam.tscn")
var shuriken_scene := preload("res://scenes/shuriken.tscn")
var target: Node2D = null
var all_segs: Array = []
var mid_segs: Array = []
@ -123,6 +124,10 @@ func retarget(new_target: Node2D) -> void:
func do_damage_tick() -> void:
if is_instance_valid(target) and not target.is_dying:
target.take_damage(PRIMARY_TICK_DMG)
if perk_effects.laser_shuriken_on_kill and target.is_dying:
var sh = shuriken_scene.instantiate()
sh.global_position = target.global_position
get_parent().add_child(sh)
var hit: Array = []
for seg in mid_segs:

View File

@ -8,6 +8,11 @@ var cauldron
var available_perks: Array[Perk] = []
var spell_modes: Dictionary = {}
var laser_retarget_enabled = false
var shuriken_tag_enabled = false
var tornado_push_on_cast = false
var tornado_fruit_collect = false
var laser_shuriken_on_kill = false
var _icon_fire_swirl: AtlasTexture
var throwing_knife_enabled = false
var throwing_knife_cooldown: float = 2.0
var throwing_knife_count: int = 1
@ -24,6 +29,7 @@ var _spellbooks: Array = []
var _icon_knife = preload("res://assets/weapons/knvie.png")
var _icon_laser: AtlasTexture
var _icon_tornado: 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
@ -64,6 +70,14 @@ func _ready() -> void:
_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)
_icon_tornado = AtlasTexture.new()
_icon_tornado.atlas = preload("res://assets/Free Effect Bullet Impact Explosion 32x32 V1/Purple Effect Bullet Impact Explosion 32x32.png")
_icon_tornado.region = Rect2(192, 0, 32, 32)
_icon_fire_swirl = AtlasTexture.new()
_icon_fire_swirl.atlas = preload("res://assets/Free Effect Bullet Impact Explosion 32x32 V1/Red Effect Bullet Impact Explosion 32x32.png")
_icon_fire_swirl.region = Rect2(352, 64, 32, 32)
var lrt = Perk.new()
lrt.name = "Laser Lock-On"
lrt.description = "Laser retargets to the next highest health enemy on kill"
@ -197,6 +211,52 @@ func _ready() -> void:
bh.effect = battle_hardened
available_perks.append(bh)
var rp = Perk.new()
rp.name = "Reaper's Pull"
rp.description = "Tornado continuously collects nearby fruits for its entire lifetime"
rp.stats = _stat_toggle("Auto-Collect")
rp.spell = SpellLibrary.TORNADO
rp.icon = _icon_tornado
rp.effect = enable_tornado_fruit_collect
available_perks.append(rp)
var rw = Perk.new()
rw.name = "Repulsion Wave"
rw.description = "Casting Tornado blasts all enemies away from the witch"
rw.stats = _stat_toggle("Push on Cast")
rw.spell = SpellLibrary.TORNADO
rw.icon = _icon_tornado
rw.effect = enable_tornado_push
available_perks.append(rw)
var ssp = Perk.new()
ssp.name = "Spiral Swirl"
ssp.description = "Fire Swirl fires all 8 swirls simultaneously in a rotating spiral pattern"
ssp.stats = _stat_toggle("Spiral")
ssp.spell = SpellLibrary.FIRE_SWIRL
ssp.archetype_slot = "fire_swirl.cast"
ssp.icon = _icon_fire_swirl
ssp.effect = enable_fire_swirl_spiral
available_perks.append(ssp)
var lsk = Perk.new()
lsk.name = "Chain Kill"
lsk.description = "Laser spawns a free Shuriken at every enemy it kills"
lsk.stats = _stat_toggle("Shuriken on Kill")
lsk.spell = SpellLibrary.LASER
lsk.icon = _icon_laser
lsk.effect = enable_laser_shuriken_on_kill
available_perks.append(lsk)
var mp = Perk.new()
mp.name = "Marked Prey"
mp.description = "Shuriken marks enemies. The next spell that hits them triggers an extra explosion"
mp.stats = _stat_toggle("Mark on Hit")
mp.spell = SpellLibrary.SHURIKEN
mp.icon = _icon_shuriken
mp.effect = enable_marked_prey
available_perks.append(mp)
var ss = Perk.new()
ss.name = "Swift Strike"
ss.description = "Attack more often"
@ -221,6 +281,21 @@ func _process(delta: float) -> void:
func laser_retarget():
laser_retarget_enabled = true
func enable_marked_prey() -> void:
shuriken_tag_enabled = true
func enable_tornado_push() -> void:
tornado_push_on_cast = true
func enable_tornado_fruit_collect() -> void:
tornado_fruit_collect = true
func enable_laser_shuriken_on_kill() -> void:
laser_shuriken_on_kill = true
func enable_fire_swirl_spiral() -> void:
set_mode("fire_swirl.cast", "spiral")
func double_shuriken():
witch.shuriken_count += 2

View File

@ -1,8 +1,8 @@
extends ProjectileBase
var enemies_hit = 0
@onready var perk_effects = get_node("/root/Game/PerkEffects")
func _ready() -> void:
speed = 500
super()
@ -17,6 +17,9 @@ func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("enemies") and not body.is_hurt and not body.is_dying:
enemies_hit += 1
body.take_damage(damage)
if perk_effects.shuriken_tag_enabled and not body.is_dying:
body.is_tagged = true
body.modulate = Color(1.0, 1.0, 0.45)
if enemies_hit == 20:
queue_free()
else:

View File

@ -9,6 +9,7 @@ extends Area2D
@onready var explosion_collision: CollisionShape2D = $ExplosionCollision
var _is_pulling := true
@onready var perk_effects = get_node("/root/Game/PerkEffects")
func _ready() -> void:
tornado_area.disabled = false
@ -19,6 +20,8 @@ func _ready() -> void:
_explode()
func _physics_process(delta: float) -> void:
if perk_effects.tornado_fruit_collect:
_collect_nearby_fruits()
if not _is_pulling:
return
for enemy in get_overlapping_bodies():
@ -28,6 +31,15 @@ func _physics_process(delta: float) -> void:
continue
enemy.global_position = enemy.global_position.move_toward(global_position, pull_strength * delta)
func _collect_nearby_fruits() -> void:
var drop_manager = get_node_or_null("/root/Game/DropManager")
if drop_manager == null:
return
for child in drop_manager.get_children():
if child is DropsBase and not child.is_spawning and not child.in_orbit:
if global_position.distance_to(child.global_position) < 120.0:
child.collect()
func _explode() -> void:
_is_pulling = false
var targets = get_overlapping_bodies().filter(func(b): return b.is_in_group("enemies") and not b.is_dying)

View File

@ -150,6 +150,11 @@ func shoot_shuriken():
func shoot_tornado():
StatsManager.on_spell_cast("tornado")
if perk_effects.tornado_push_on_cast:
for enemy in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(enemy) and not enemy.is_dying:
var push_dir = global_position.direction_to(enemy.global_position)
enemy.apply_push(push_dir, 500.0)
var target = get_nearest_enemy(global_position)
var tw = tornado.instantiate()
tw.global_position = target.global_position if target != null else global_position