Artur David 2026-06-02 17:50:10 +02:00
commit c587fe9e4f
25 changed files with 555 additions and 54 deletions

Binary file not shown.

View File

@ -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

View File

@ -1,26 +1,28 @@
[ [
{ {
"time_start": 0, "time_start": 0,
"time_end": 60, "time_end": 60,
"entries": [ "elite_enemy": "res://scenes/slime.tscn",
{ "enemy": "res://scenes/slime.tscn", "count_at_start": 0, "count_at_end": 15, "min_interval": 0.5 } "entries": [
] { "enemy": "res://scenes/slime.tscn", "count_at_start": 0, "count_at_end": 15, "min_interval": 0.5 }
]
}, },
{ {
"time_start": 60, "time_start": 60,
"time_end": 180, "time_end": 180,
"entries": [ "elite_enemy": "res://scenes/blue_slime.tscn",
{ "enemy": "res://scenes/slime.tscn", "count_at_start": 15, "count_at_end": 40, "min_interval": 0.3 }, "entries": [
{ "enemy": "res://scenes/blue_slime.tscn", "count_at_start": 0, "count_at_end": 10, "min_interval": 0.8 } { "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_start": 180,
"time_end": -1, "time_end": -1,
"entries": [ "entries": [
{ "enemy": "res://scenes/slime.tscn", "count_at_start": 40, "count_at_end": 100, "min_interval": 0.2 }, { "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/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 } { "enemy": "res://scenes/fire_slime.tscn", "count_at_start": 0, "count_at_end": 40, "min_interval": 0.6 }
] ]
} }
] ]

View File

@ -34,6 +34,33 @@ window/stretch/mode="viewport"
3d/physics_engine="Jolt Physics" 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]
rendering_device/driver.windows="d3d12" rendering_device/driver.windows="d3d12"

122
scenes/beam.tscn 100644
View File

@ -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

22
scenes/chili.tscn 100644
View File

@ -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")

View File

@ -1,6 +1,8 @@
[gd_scene format=3 uid="uid://cpe6aiuqiox0u"] [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://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://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://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"] [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"] [sub_resource type="CircleShape2D" id="CircleShape2D_odbmi"]
radius = 8.062258 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] [node name="FireSlime" type="CharacterBody2D" unique_id=1827403107]
script = ExtResource("1_88j2t") script = ExtResource("1_88j2t")
metadata/_custom_type_script = "uid://c0uv02nt5ocvg" 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] [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="." unique_id=1151813585]
texture_filter = 1 texture_filter = 1

View File

@ -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")

View File

@ -4,6 +4,7 @@ class_name DropsBase
var witch var witch
var player var player
var is_spawning = true var is_spawning = true
var in_orbit: bool = false
signal collected signal collected
var _collect_sfx = preload("res://assets/music&sfx/sfx/lesiakower-coin-collect-retro-8-bit-sound-effect-145251.mp3") 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 is_spawning = false
func _on_body_entered(body: Node2D) -> void: func _on_body_entered(body: Node2D) -> void:
if body == player and not is_spawning: if body == player and not is_spawning and not in_orbit:
collect() var cauldron = witch.get_node("CauldronBar")
pass 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(): func collect():
_sfx_player.pitch_scale = randf_range(0.85, 1.15) _sfx_player.pitch_scale = randf_range(0.85, 1.15)

View File

@ -2,5 +2,6 @@ extends Resource
class_name SpawnStage class_name SpawnStage
@export var time_start: float = 0.0 @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] @export var entries: Array[StageEntry]

View File

@ -2,29 +2,31 @@ extends Node
const APPLE = 0 const APPLE = 0
const GRAPE = 1 const GRAPE = 1
const CHILI = 2
const NONE = "NONE" const NONE = "NONE"
const SHURIKEN = "SHURIKEN" const SHURIKEN = "SHURIKEN"
const FIREBALL = "FIREBALL" const FIREBALL = "FIREBALL"
const FIRE_SWIRL = "FIRE_SWIRL" const FIRE_SWIRL = "FIRE_SWIRL"
const TORNADO = "TORNADO" const TORNADO = "TORNADO"
const LASER = "LASER"
# Each spell's display recipe: sorted apples-first, grapes-last
var recipes: Dictionary = { var recipes: Dictionary = {
SHURIKEN: [APPLE, GRAPE, GRAPE], FIREBALL: [APPLE],
FIREBALL: [APPLE, APPLE, APPLE], TORNADO: [GRAPE],
FIRE_SWIRL: [APPLE, APPLE, GRAPE], LASER: [CHILI],
TORNADO: [GRAPE, GRAPE, GRAPE], 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: func identify(cauldron_slots: Array) -> String:
var apples = cauldron_slots.count(2) var types: Array = []
var grapes = cauldron_slots.count(4) 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: for spell_id in recipes:
var r: Array = recipes[spell_id] if recipes[spell_id] == types:
if r.count(APPLE) == apples and r.count(GRAPE) == grapes:
return spell_id return spell_id
return NONE return NONE

11
scripts/beam.gd 100644
View File

@ -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"

View File

@ -0,0 +1 @@
uid://bdhx27edemfce

View File

@ -5,6 +5,7 @@ func _ready() -> void:
speed = 15.0 speed = 15.0
max_hp = 25 max_hp = 25
hp = max_hp hp = max_hp
damage = 5
# $Area2D.body_entered.connect(_on_area_2d_body_entered) # $Area2D.body_entered.connect(_on_area_2d_body_entered)
func _process(delta: float) -> void: func _process(delta: float) -> void:

View File

@ -13,6 +13,7 @@ var progres_index = 0
var is_brewing var is_brewing
var brew_explosion = true var brew_explosion = true
@onready var witch = get_parent() @onready var witch = get_parent()
@onready var player = get_node("/root/Game/Player")
var explosion_scene = preload("res://scenes/explosion.tscn") 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_sfx = preload("res://assets/music&sfx/sfx/data_pion-sfx9-fwoosh-324525.mp3")
var _ignite_player: AudioStreamPlayer var _ignite_player: AudioStreamPlayer
@ -87,6 +88,8 @@ func progres_bar(fruit):
change_texture(progres_index, 2) change_texture(progres_index, 2)
if fruit is Grape: if fruit is Grape:
change_texture(progres_index, 4) change_texture(progres_index, 4)
if fruit is Chili:
change_texture(progres_index, 3)
_pop_slot(progres_index) _pop_slot(progres_index)
progres_index += 1 progres_index += 1
if progres_index == 3: if progres_index == 3:
@ -117,9 +120,11 @@ func brew(fruits):
SpellLibrary.FIREBALL: witch.shoot_fireballs() SpellLibrary.FIREBALL: witch.shoot_fireballs()
SpellLibrary.FIRE_SWIRL: witch.shoot_fire_swirl() SpellLibrary.FIRE_SWIRL: witch.shoot_fire_swirl()
SpellLibrary.TORNADO: witch.shoot_tornado() SpellLibrary.TORNADO: witch.shoot_tornado()
SpellLibrary.LASER: witch.shoot_laser()
reset_texture() reset_texture()
is_brewing = false is_brewing = false
player.flush_queue()
func get_unique_fruits() -> Array: func get_unique_fruits() -> Array:
var unique = [] var unique = []
for fruit in slot_states: for fruit in slot_states:

2
scripts/chili.gd 100644
View File

@ -0,0 +1,2 @@
extends DropsBase
class_name Chili

View File

@ -0,0 +1 @@
uid://dinqfnri3co88

View File

@ -3,6 +3,13 @@ extends CanvasLayer
@onready var _spawn_control = get_node("/root/Game/SpawnControl") @onready var _spawn_control = get_node("/root/Game/SpawnControl")
@onready var _witch = get_node("/root/Game/Witch") @onready var _witch = get_node("/root/Game/Witch")
@onready var _drop_manager = get_node("/root/Game/DropManager") @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 _spell_dispatch: Dictionary
var _lvl_disable_btn: Button var _lvl_disable_btn: Button
@ -14,6 +21,7 @@ func _ready() -> void:
SpellLibrary.SHURIKEN: _witch.shoot_shuriken, SpellLibrary.SHURIKEN: _witch.shoot_shuriken,
SpellLibrary.FIRE_SWIRL: _witch.shoot_fire_swirl, SpellLibrary.FIRE_SWIRL: _witch.shoot_fire_swirl,
SpellLibrary.TORNADO: _witch.shoot_tornado, SpellLibrary.TORNADO: _witch.shoot_tornado,
SpellLibrary.LASER: _witch.shoot_laser,
} }
_build_ui() _build_ui()
hide() hide()
@ -72,6 +80,10 @@ func _build_ui() -> void:
btn.pressed.connect(_skip_time.bind(float(secs))) btn.pressed.connect(_skip_time.bind(float(secs)))
hbox_time.add_child(btn) 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") _add_section(vbox, "SPELLS")
for spell_id in SpellLibrary.recipes.keys(): for spell_id in SpellLibrary.recipes.keys():
if _spell_dispatch.has(spell_id): if _spell_dispatch.has(spell_id):
@ -99,6 +111,11 @@ func _add_button(parent: VBoxContainer, label: String, callback: Callable) -> vo
btn.pressed.connect(callback) btn.pressed.connect(callback)
parent.add_child(btn) 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: func _kill_all_enemies() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"): for enemy in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(enemy): if is_instance_valid(enemy):

View File

@ -5,6 +5,7 @@ func _ready() -> void:
speed = 15.0 speed = 15.0
max_hp = 50 max_hp = 50
hp = max_hp hp = max_hp
damage = 10
$Area2D.body_entered.connect(_on_area_2d_body_entered) $Area2D.body_entered.connect(_on_area_2d_body_entered)
func _process(delta: float) -> void: func _process(delta: float) -> void:

140
scripts/laser.gd 100644
View File

@ -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

View File

@ -0,0 +1 @@
uid://dp6b3imslv10d

View File

@ -7,6 +7,7 @@ var throwing_knife = preload("res://scenes/throwing_knive.tscn")
var cauldron var cauldron
var available_perks: Array[Perk] = [] var available_perks: Array[Perk] = []
var fireball_aoe_enabled = false var fireball_aoe_enabled = false
var laser_retarget_enabled = false
var throwing_knife_enabled = false var throwing_knife_enabled = false
var throwing_knife_cooldown: float = 2.0 var throwing_knife_cooldown: float = 2.0
var throwing_knife_count: int = 1 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_scene = preload("res://scenes/spellbook.tscn")
var spellbook_count: int = 0 var spellbook_count: int = 0
var spellbook_damage: int = 12 var spellbook_damage: int = 12
const SPELLBOOK_MAX_DAMAGE: int = 40
var spellbook_speed: float = 1.5 var spellbook_speed: float = 1.5
const SPELLBOOK_RADIUS: float = 60.0 const SPELLBOOK_RADIUS: float = 60.0
var _spellbook_angle: float = 0.0 var _spellbook_angle: float = 0.0
var _spellbooks: Array = [] var _spellbooks: Array = []
var _icon_knife = preload("res://assets/weapons/knvie.png") 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_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_brew = preload("res://assets/books_set_2/books_health_potion.png")
var _icon_shuriken: AtlasTexture 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.atlas = preload("res://assets/Swordsman_lvl1/Without_shadow/Swordsman_lvl1_Idle_without_shadow.png")
_icon_player.region = Rect2(0, 0, 64, 64) _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() var dsh = Perk.new()
dsh.name = "Double Shuriken" dsh.name = "Double Shuriken"
dsh.description = "Fire two shurikens at once" dsh.description = "Fire two more shurikens at once"
dsh.stats = _stat("Shurikens", "1", "2") dsh.stats = _stat("Shurikens", str(witch.shuriken_count), str(witch.shuriken_count + 2))
dsh.spell = SpellLibrary.SHURIKEN dsh.spell = SpellLibrary.SHURIKEN
dsh.icon = _icon_shuriken dsh.icon = _icon_shuriken
dsh.effect = double_shuriken dsh.effect = double_shuriken
@ -69,6 +85,15 @@ func _ready() -> void:
faoe.effect = fireball_aoe faoe.effect = fireball_aoe
available_perks.append(faoe) 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() var bexp = Perk.new()
bexp.name = "Brew Explosion" bexp.name = "Brew Explosion"
bexp.description = "Trigger an explosion on brew" bexp.description = "Trigger an explosion on brew"
@ -135,12 +160,18 @@ func _process(delta: float) -> void:
var offset = (TAU / _spellbooks.size()) * i var offset = (TAU / _spellbooks.size()) * i
_spellbooks[i].global_position = witch.global_position + Vector2(cos(_spellbook_angle + offset), sin(_spellbook_angle + offset)) * SPELLBOOK_RADIUS _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(): func double_shuriken():
witch.shuriken_count = 2 witch.shuriken_count += 2
func fireball_aoe(): func fireball_aoe():
fireball_aoe_enabled = true fireball_aoe_enabled = true
func fireball_spread():
witch.fireball_max_targets += 3
func brew_explosion(): func brew_explosion():
cauldron.brew_explosion = true cauldron.brew_explosion = true
@ -237,16 +268,17 @@ func faster_orbit() -> void:
available_perks.append(fo) available_perks.append(fo)
func book_damage() -> void: func book_damage() -> void:
spellbook_damage += 4 spellbook_damage = min(spellbook_damage + 4, SPELLBOOK_MAX_DAMAGE)
for book in _spellbooks: for book in _spellbooks:
book.damage = spellbook_damage book.damage = spellbook_damage
var bd = Perk.new() if spellbook_damage < SPELLBOOK_MAX_DAMAGE:
bd.name = "Book Damage" var bd = Perk.new()
bd.description = "Books hit harder" bd.name = "Book Damage"
bd.stats = _stat("Damage", str(spellbook_damage), str(spellbook_damage + 4)) bd.description = "Books hit harder"
bd.icon = _icon_book bd.stats = _stat("Damage", str(spellbook_damage), str(min(spellbook_damage + 4, SPELLBOOK_MAX_DAMAGE)))
bd.effect = book_damage bd.icon = _icon_book
available_perks.append(bd) bd.effect = book_damage
available_perks.append(bd)
func _rebuild_spellbooks() -> void: func _rebuild_spellbooks() -> void:
for book in _spellbooks: for book in _spellbooks:

View File

@ -6,11 +6,22 @@ var level = 1
var speed = 60 var speed = 60
var strength = 3 var strength = 3
var attacks = false 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): 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 velocity = direction * speed
move_and_slide() 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: if attacks == true:
return return
if direction == Vector2.ZERO: 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: func _on_melee_area_body_entered(body: Node2D) -> void:
if body.is_in_group("enemies"): if body.is_in_group("enemies"):
if $AttackSpeed.is_stopped(): if $AttackSpeed.is_stopped():

View File

@ -8,11 +8,13 @@ var elapsed_time: float = 0.0
const STAGES_JSON = "res://data/spawn_stages.json" 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) var stages: Array[SpawnStage] = []
# values: { "timer": float, "alive": int }
var _state: Dictionary = {} var _state: Dictionary = {}
var current_stage_idx: int = -1
func _ready() -> void: func _ready() -> void:
var camera: Camera2D = get_parent().get_node("Camera2D") var camera: Camera2D = get_parent().get_node("Camera2D")
@ -44,6 +46,8 @@ func _load_stages(path: String) -> void:
var stage = SpawnStage.new() var stage = SpawnStage.new()
stage.time_start = float(sd["time_start"]) stage.time_start = float(sd["time_start"])
stage.time_end = float(sd["time_end"]) stage.time_end = float(sd["time_end"])
if sd.has("elite_enemy"):
stage.elite_enemy = load(sd["elite_enemy"])
for ed in sd["entries"]: for ed in sd["entries"]:
var entry = StageEntry.new() var entry = StageEntry.new()
entry.enemy = load(ed["enemy"]) entry.enemy = load(ed["enemy"])
@ -72,9 +76,31 @@ func get_spawn_position() -> Vector2:
spawn_y = randf_range(up_right.y, down_right.y) spawn_y = randf_range(up_right.y, down_right.y)
return Vector2(spawn_x, spawn_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: func _process(delta: float) -> void:
elapsed_time += delta 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(): for si in stages.size():
var stage: SpawnStage = stages[si] var stage: SpawnStage = stages[si]
if elapsed_time < stage.time_start: if elapsed_time < stage.time_start:

View File

@ -8,12 +8,15 @@ var fireball = preload("res://scenes/fireball.tscn")
var shuriken = preload("res://scenes/shuriken.tscn") var shuriken = preload("res://scenes/shuriken.tscn")
var fire_swirl = preload("res://scenes/fire_swirl.tscn") var fire_swirl = preload("res://scenes/fire_swirl.tscn")
var tornado = preload("res://scenes/tornado.tscn") var tornado = preload("res://scenes/tornado.tscn")
var laser = preload("res://scenes/laser.tscn")
var shuriken_count = 1 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 max_hp: int = 50
var current_hp: int = 100 var current_hp: int = 50
var is_invincible: bool = false var is_invincible: bool = false
var fireball_max_targets: int = 5
const HP_BAR_WIDTH = 20 const HP_BAR_WIDTH = 20
const HP_BAR_HEIGHT = 3 const HP_BAR_HEIGHT = 3
@ -51,7 +54,10 @@ func _on_collect(DropsBase):
func shoot_fireballs(): func shoot_fireballs():
var enemies = get_tree().get_nodes_in_group("enemies") 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): if not is_instance_valid(enemy):
continue continue
var dir = global_position.direction_to(enemy.global_position) 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 tw.global_position = target.global_position if target != null else global_position
get_parent().add_child(tw) get_parent().add_child(tw)
camera.shake(0.3, 0.8) 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: func take_damage(amount: int) -> void:
if is_invincible: if is_invincible:
@ -111,7 +130,7 @@ func take_damage(amount: int) -> void:
get_tree().call_deferred("reload_current_scene") get_tree().call_deferred("reload_current_scene")
return return
is_invincible = true is_invincible = true
await get_tree().create_timer(1.0).timeout await get_tree().create_timer(0.5).timeout
is_invincible = false is_invincible = false
func get_nearest_enemy(from: Vector2, filter: Callable = Callable()) -> Node: func get_nearest_enemy(from: Vector2, filter: Callable = Callable()) -> Node: