Compare commits

..

17 Commits

Author SHA1 Message Date
Georgiy Gorin 221727289d New pics 2026-05-06 20:23:07 +03:00
DragonSpirit 1baf3e047b latest prompts by @gera 2026-05-06 09:06:08 +03:00
Nikolay Fedorov 849b0b7315 fix sound 2026-04-23 19:05:10 +03:00
DragonSpirit 1c70488ea5 add animation on movement 2026-04-23 18:53:50 +03:00
Georgiy Gorin 480d355926 Sound 2026-04-23 18:53:25 +03:00
Nikolay Fedorov 229a2cca60 add walk animation 2026-04-23 18:50:01 +03:00
Nikolay Fedorov b0ef832c64 fix rotation 2026-04-23 18:39:58 +03:00
DragonSpirit f28f21d5b4 add sounds, add logo 2026-04-23 18:32:57 +03:00
Nikolay Fedorov 69babae913 update game name 2026-04-23 18:23:11 +03:00
DragonSpirit e3261f8e78 update tutors 2026-04-23 18:22:37 +03:00
DragonSpirit 794261e0d0 win and lose cheat hotkey 2026-04-23 18:16:26 +03:00
DragonSpirit 187fe7a9ce mouse save 2026-04-23 18:14:14 +03:00
DragonSpirit c201a67bb8 add kick animation by enemy 2026-04-23 18:11:03 +03:00
Georgiy Gorin 7475339ddb recepie 2026-04-23 18:00:17 +03:00
Nikolay Fedorov dd60de0a66 update shape order 2026-04-23 17:48:23 +03:00
Nikolay Fedorov 912bffd59a Merge branch 'exp' of https://git.nfedorov.dev/DragonSpirit/KickSurvivors into exp 2026-04-23 17:45:50 +03:00
Nikolay Fedorov a2e710575e import new gnomes 2026-04-23 17:45:17 +03:00
40 changed files with 1036 additions and 117 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://un6qkfobvo2o"
path="res://.godot/imported/Pause_Controls.jpeg-7b152fd8be8bb847576ebbef7cd65320.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/Pause_Controls.jpeg"
dest_files=["res://.godot/imported/Pause_Controls.jpeg-7b152fd8be8bb847576ebbef7cd65320.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cavydxru3563h"
path="res://.godot/imported/Pause_Craft.jpeg-62e6f52659e81a185c138647d52f9aef.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/Pause_Craft.jpeg"
dest_files=["res://.godot/imported/Pause_Craft.jpeg-62e6f52659e81a185c138647d52f9aef.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+3 -3
View File
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://baeea1yfs0cnn"
path="res://.godot/imported/Tutorial_shield.jpeg-d9b563c9ca34dc1a19e82a0ec964c1b9.ctex"
path="res://.godot/imported/Tutorial_Shield.jpeg-6b96e6b9716aced42153397e827ce868.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/Tutorial_shield.jpeg"
dest_files=["res://.godot/imported/Tutorial_shield.jpeg-d9b563c9ca34dc1a19e82a0ec964c1b9.ctex"]
source_file="res://assets/Tutorial_Shield.jpeg"
dest_files=["res://.godot/imported/Tutorial_Shield.jpeg-6b96e6b9716aced42153397e827ce868.ctex"]
[params]
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://boyy2vpjoxb6l"
path="res://.godot/imported/ambient.ogg-35a37efc4b30706a227e17f0453e3a99.oggvorbisstr"
[deps]
source_file="res://assets/ambient.ogg"
dest_files=["res://.godot/imported/ambient.ogg-35a37efc4b30706a227e17f0453e3a99.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://82lpmyibj8tp"
path="res://.godot/imported/boot_walk.glb-d86cd37ca982d86b2ebd647af9a26482.scn"
[deps]
source_file="res://assets/boot_walk.glb"
dest_files=["res://.godot/imported/boot_walk.glb-d86cd37ca982d86b2ebd647af9a26482.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
BIN
View File
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c743vb62pekwt"
path="res://.godot/imported/damage.ogg-137ecbb71d5ea483a0892197341e95d9.oggvorbisstr"
[deps]
source_file="res://assets/damage.ogg"
dest_files=["res://.godot/imported/damage.ogg-137ecbb71d5ea483a0892197341e95d9.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
BIN
View File
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cqtr8862yg1pw"
path="res://.godot/imported/gnome2.glb-ed506cf1ac0ccb2d264457e28d22df82.scn"
[deps]
source_file="res://assets/gnome2.glb"
dest_files=["res://.godot/imported/gnome2.glb-ed506cf1ac0ccb2d264457e28d22df82.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
BIN
View File
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://d38xq7m2ov1ig"
path="res://.godot/imported/gnome3.glb-60a565f65ec090ccf38c4f1b4928e113.scn"
[deps]
source_file="res://assets/gnome3.glb"
dest_files=["res://.godot/imported/gnome3.glb-60a565f65ec090ccf38c4f1b4928e113.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://d0ygls7mtp1q1"
path="res://.godot/imported/growling-cartoon-animal-in-the-game.ogg-f3dde05a2449a5cb1bbbeaf8a5c195ca.oggvorbisstr"
[deps]
source_file="res://assets/growling-cartoon-animal-in-the-game.ogg"
dest_files=["res://.godot/imported/growling-cartoon-animal-in-the-game.ogg-f3dde05a2449a5cb1bbbeaf8a5c195ca.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://bqj5f858tqy5w"
path="res://.godot/imported/kick_enemy.ogg-47fb08fccd1ea50bf15fb26d6abda71e.oggvorbisstr"
[deps]
source_file="res://assets/kick_enemy.ogg"
dest_files=["res://.godot/imported/kick_enemy.ogg-47fb08fccd1ea50bf15fb26d6abda71e.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://cq4hup0lwti13"
path="res://.godot/imported/kick_player.ogg-ed43ff774ec5730f331b64274b81fa27.oggvorbisstr"
[deps]
source_file="res://assets/kick_player.ogg"
dest_files=["res://.godot/imported/kick_player.ogg-ed43ff774ec5730f331b64274b81fa27.oggvorbisstr"]
[params]
loop=true
loop_offset=12.0
bpm=0.0
beat_count=0
bar_beats=4
BIN
View File
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://djmceowwiaxh"
path="res://.godot/imported/merge.ogg-23e958ec7fe7428616d58e4721608c5d.oggvorbisstr"
[deps]
source_file="res://assets/merge.ogg"
dest_files=["res://.godot/imported/merge.ogg-23e958ec7fe7428616d58e4721608c5d.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cx7dvk0afwj6h"
path="res://.godot/imported/player_man_walk.glb-3657f9b02230b13d6d797514cd5d2eb0.scn"
[deps]
source_file="res://assets/player_man_walk.glb"
dest_files=["res://.godot/imported/player_man_walk.glb-3657f9b02230b13d6d797514cd5d2eb0.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://baavv8uqo25la"
path="res://.godot/imported/studio_logo.png-f3d319855b8a77cbfc974454f8f0d5d5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/studio_logo.png"
dest_files=["res://.godot/imported/studio_logo.png-f3d319855b8a77cbfc974454f8f0d5d5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+1 -1
View File
@@ -8,7 +8,7 @@ custom_features=""
export_filter="all_resources"
include_filter="*jpeg"
exclude_filter=""
export_path="../Kick/KickSurvivors.exe"
export_path="../Kick/CraftKick.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
+183 -21
View File
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -1,8 +1,10 @@
[gd_scene format=3 uid="uid://mainmenu2024"]
[gd_scene format=3 uid="uid://jgfauchpwiii"]
[ext_resource type="Script" path="res://scripts/MainMenu.gd" id="1_menu"]
[ext_resource type="Script" uid="uid://on1o20vpycgm" path="res://scripts/MainMenu.gd" id="1_menu"]
[node name="MainMenu" type="Control"]
[node name="MainMenu" type="Control" unique_id=495626975]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_menu")
+150 -50
View File
File diff suppressed because one or more lines are too long
+45 -13
View File
@@ -45,10 +45,13 @@ var merge_partner: Node = null
var is_upgrading: bool = false
@onready var mesh_node: MeshInstance3D = $BodyMesh
#@onready var mesh_node1: MeshInstance3D = $BodyMesh1
#@onready var mesh_node2: MeshInstance3D = $BodyMesh2
@onready var mesh_node1: MeshInstance3D = $BodyMesh1
@onready var mesh_node2: MeshInstance3D = $BodyMesh2
@onready var anim_player: AnimationPlayer = $AnimationPlayer
var mat: StandardMaterial3D
var type_label: Label3D
var active_mesh: MeshInstance3D
var COLOR_CHASE = Color(1.0, 0.28, 0.18)
@@ -61,7 +64,9 @@ const AIR_FRICTION = 0.86
func _ready() -> void:
process_mode = Node.PROCESS_MODE_PAUSABLE
add_to_group("enemies")
mat = mesh_node.material_override.duplicate() as StandardMaterial3D
active_mesh = mesh_node
var orig := mesh_node.material_override
mat = (orig.duplicate() if orig != null else StandardMaterial3D.new()) as StandardMaterial3D
mesh_node.material_override = mat
#mesh_node1.set_visible(false)
#mesh_node2.set_visible(false)
@@ -123,19 +128,35 @@ func setup(type: String, wave: int) -> void:
_apply_mesh(enemy_level)
func _apply_mesh(level: int) -> void:
var idx := clampi(level, 1, 3)
var mesh_res := load("res://assets/gnome%d.obj" % idx) as Mesh
if mesh_res == null:
var idx := clampi(level, 1, 3)
var scene := load("res://assets/gnome%d.glb" % idx) as PackedScene
if scene == null:
return
var new_mat := StandardMaterial3D.new()
var tex := load("res://assets/gnome%d.png" % idx) as Texture2D
if tex != null:
new_mat.albedo_texture = tex
var root := scene.instantiate()
var src: MeshInstance3D = null
for child in root.get_children():
var mi := child as MeshInstance3D
if mi != null:
src = mi
break
if src == null:
root.queue_free()
return
mesh_node.mesh = src.mesh
var new_mat: StandardMaterial3D
if src.get_surface_override_material(0) != null:
new_mat = src.get_surface_override_material(0).duplicate() as StandardMaterial3D
else:
new_mat = StandardMaterial3D.new()
var tex := load("res://assets/gnome%d.png" % idx) as Texture2D
if tex != null:
new_mat.albedo_texture = tex
new_mat.albedo_color = Color.WHITE
mesh_node.mesh = mesh_res
mesh_node.material_override = new_mat
root.queue_free()
mat = new_mat
COLOR_CHASE = Color.WHITE
active_mesh = mesh_node
func _physics_process(delta: float) -> void:
match state:
@@ -155,6 +176,7 @@ func _chase(delta: float) -> void:
var dist := diff.length()
if dist < 1.0 and contact_timer <= 0.0:
contact_timer = CONTACT_CD
_play_kick_blend()
if target.has_method("take_damage"):
target.take_damage(damage_to_player)
FX.hit_spark(target.global_position + Vector3(0, 0.5, 0), get_parent(), Color(0.95, 0.18, 0.08))
@@ -212,6 +234,7 @@ func _try_enemy_kick() -> void:
nearest_dist = d.length()
nearest_kickable = k
if nearest_kickable != null:
_play_kick_blend()
nearest_kickable.call("receive_kick", kick_dir, 35.0 + kick_tier * 8.0)
FX.hit_spark(nearest_kickable.global_position + Vector3(0, 0.4, 0), get_parent(), Color(1.0, 0.72, 0.1))
kickable_kick_timer = KICKABLE_KICK_COOLDOWN
@@ -233,6 +256,7 @@ func _try_enemy_kick() -> void:
nearest_enemy_dist = d.length()
nearest_enemy = en
if nearest_enemy != null:
_play_kick_blend()
nearest_enemy.call("receive_kick", kick_dir, 40.0 + kick_tier * 10.0)
enemy_kick_timer = ENEMY_KICK_COOLDOWN
return
@@ -242,8 +266,7 @@ func _try_enemy_kick() -> void:
if to_player.length() < ENEMY_KICK_RANGE:
var player_toughness: int = target.get("toughness_tier") if target.get("toughness_tier") != null else 0
if player_toughness < kick_tier:
# light kick
# target.call("receive_kick", kick_dir, 12.0 + kick_tier * 10.0)
_play_kick_blend()
target.call("receive_kick", kick_dir, 35.0 + kick_tier * 8.0)
FX.hit_spark(target.global_position + Vector3(0, 0.5, 0), get_parent(), Color(0.95, 0.18, 0.08))
enemy_kick_timer = ENEMY_KICK_COOLDOWN
@@ -373,6 +396,9 @@ func _enter_stun() -> void:
func _enter_chase() -> void:
state = State.CHASING
mat.albedo_color = COLOR_CHASE
mesh_node.rotation.y = 0.0
mesh_node1.rotation.y = 0.0
mesh_node2.rotation.y = 0.0
func _take_hit(dmg: int) -> void:
if state == State.DEAD:
@@ -381,6 +407,12 @@ func _take_hit(dmg: int) -> void:
if health <= 0:
_die()
func _play_kick_blend() -> void:
SFX.kick_enemy(get_parent())
if anim_player != null and anim_player.has_animation("kick"):
anim_player.stop()
anim_player.play("kick")
func _wall_impact_effect() -> void:
var tw := create_tween()
tw.tween_property(mat, "albedo_color", Color.WHITE, 0.04)
+2
View File
@@ -66,3 +66,5 @@ static func _execute_recipe(a: Node3D, b: Node3D, recipe: Dictionary) -> void:
parent.add_child(result)
result.global_position = pos
FX.merge_smoke(pos + Vector3(0, 0.3, 0), parent)
SFX.merge(parent)
parent.get_tree().create_timer(30.0).connect("timeout", result.queue_free)
-3
View File
@@ -23,6 +23,3 @@ func _process(delta: float) -> void:
func interact(player: Node) -> void:
player.call("apply_upgrade_boots", 2.0, tier)
queue_free()
var mains := get_tree().get_nodes_in_group("main")
if not mains.is_empty():
mains[0].call("show_tutorial", "Tutorial_LeatherBoots")
+138 -11
View File
@@ -47,6 +47,10 @@ var progress_bar: ColorRect
var progress_bg: ColorRect
var upgrade_panel: Panel
var gameover_panel: Panel
var pause_panel: Panel
var pause_image: TextureRect
var pause_toggle_btn: Button
var _pause_showing_craft: bool = true
# Equipment slots
var equip_fills: Array[ColorRect] = []
@@ -56,6 +60,7 @@ var _equip_prev_tiers: Array[int] = [-1, -1, -1]
# Boss phase
var boss_active: bool = false
var first_boss_spawned: bool = false
var first_bat_spawned: bool = false
var boss_timer: float = 90.0
var portal_node: Node3D = null
var boss_timer_label: Label
@@ -84,10 +89,22 @@ func _input(event: InputEvent) -> void:
cam_pitch = clampf(cam_pitch, PITCH_MIN, PITCH_MAX)
if event.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
if game_active and not upgrading:
_toggle_pause_menu()
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if OS.is_debug_build():
var key := event as InputEventKey
if key != null and key.pressed and not key.echo:
if key.keycode == KEY_F5:
boss_active = true
_trigger_win()
elif key.keycode == KEY_F6:
_on_player_died()
# ─── Level ────────────────────────────────────────────────────────────────────
@@ -117,8 +134,7 @@ func _process(delta: float) -> void:
var look_at_pos := player.global_position + Vector3(0, 0.8, 0)
camera.global_position = camera.global_position.lerp(look_at_pos + offset, 14.0 * delta)
camera.look_at(look_at_pos, Vector3.UP)
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
player.set_aim_direction(deg_to_rad(cam_yaw))
player.set_aim_direction(deg_to_rad(cam_yaw))
_update_tier_label()
if boss_active:
@@ -236,6 +252,7 @@ func _spawn_player() -> void:
# ─── Game flow ────────────────────────────────────────────────────────────────
func _start_game() -> void:
SFX.start_ambient(self)
game_active = true
wave = 1
score = 0
@@ -268,6 +285,9 @@ func _spawn_enemy() -> void:
enemy.target = player
enemy.connect("died", _on_enemy_died)
enemy.connect("merged", _on_enemy_merged)
if type == "bat" and not first_bat_spawned:
first_bat_spawned = true
show_tutorial("Tutorial_LeatherBoots")
if type == "ogre" and not first_boss_spawned:
first_boss_spawned = true
_start_boss_phase()
@@ -384,9 +404,12 @@ func _pick_upgrade(id: String) -> void:
_update_labels()
func _show_gameover() -> void:
gameover_panel.visible = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
var lbl := gameover_panel.get_node("VBox/ScoreLabel") as Label
lbl.text = "Score: %d\nWave: %d" % [score, wave]
show_tutorial("LoseGame", func() -> void:
gameover_panel.visible = true
)
func _restart() -> void:
get_tree().paused = false
@@ -399,9 +422,11 @@ func _start_boss_phase() -> void:
return
boss_active = true
boss_timer = 120.0
boss_timer_label.visible = true
boss_hint_label.visible = true
_spawn_portal()
show_tutorial("ThirdLevelEnemy", func() -> void:
boss_timer_label.visible = true
boss_hint_label.visible = true
)
func _spawn_portal() -> void:
portal_node = Node3D.new()
@@ -495,9 +520,12 @@ func _trigger_win() -> void:
boss_hint_label.visible = false
if is_instance_valid(portal_node):
portal_node.queue_free()
get_tree().paused = true
win_panel.visible = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
(win_panel.get_node("VBox/ScoreLabel") as Label).text = "Score: %d\nWave: %d" % [score, wave]
show_tutorial("VictoryScreen", func() -> void:
get_tree().paused = true
win_panel.visible = true
)
func _trigger_time_up() -> void:
if not boss_active:
@@ -574,8 +602,10 @@ func _set_player_paused(paused: bool) -> void:
for e in get_tree().get_nodes_in_group("player"):
(e as Node).process_mode = mode
const _REPEATABLE_TUTORIALS := ["VictoryScreen", "LoseGame"]
func show_tutorial(key: String, on_dismiss: Callable = Callable()) -> void:
if shown_tutorials.get(key, false):
if shown_tutorials.get(key, false) and not key in _REPEATABLE_TUTORIALS:
if on_dismiss.is_valid():
on_dismiss.call()
return
@@ -613,6 +643,7 @@ func _create_ui() -> void:
_make_gameover_panel()
_make_boss_ui()
_make_win_panel()
_make_pause_panel()
func _make_hud() -> void:
score_label = _label(Vector2(16, 12), "Score: 0", 30)
@@ -626,6 +657,10 @@ func _make_recipe_panel() -> void:
"leather": "Leather",
"iron": "Iron",
"metal_plate":"Iron Plate",
#"MetalPlate":"Armor boots"
"essence":"Essence",
"Enchanted_Table": "Ench Table",
"EnchantedSphere": "Enchanted Boots"
}
const RESULT_NAMES := {
"LeatherBoots": "Leather Boots",
@@ -636,6 +671,7 @@ func _make_recipe_panel() -> void:
"WoodenShield": "Wooden Shield",
"IronShield": "Iron Shield",
"MetalArmor": "Metal Armor",
"MetalPlate": "Armor boots"
}
const PAD_H := 10
@@ -1001,3 +1037,94 @@ func _update_tier_label() -> void:
var shield_str := "-" if st == 0 else str(st)
tier_label.text = "Kick: %d Tough: %d Shield: %s" % [kt, tt, shield_str]
_update_equipment_slots()
# ─── Pause menu ───────────────────────────────────────────────────────────────
const _PAUSE_CRAFT_IMG := "res://assets/Pause_Craft.jpeg"
const _PAUSE_CONTROLS_IMG := "res://assets/Pause_Controls.jpeg"
func _make_pause_panel() -> void:
pause_panel = Panel.new()
pause_panel.process_mode = Node.PROCESS_MODE_ALWAYS
pause_panel.visible = false
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.05, 0.04, 0.10, 0.96)
sb.border_width_left = 2; sb.border_width_right = 2
sb.border_width_top = 2; sb.border_width_bottom = 2
sb.border_color = Color(0.35, 0.28, 0.55)
sb.corner_radius_top_left = 10; sb.corner_radius_top_right = 10
sb.corner_radius_bottom_left = 10; sb.corner_radius_bottom_right = 10
pause_panel.add_theme_stylebox_override("panel", sb)
pause_panel.anchor_left = 0.5; pause_panel.anchor_right = 0.5
pause_panel.anchor_top = 0.5; pause_panel.anchor_bottom = 0.5
pause_panel.offset_left = -380; pause_panel.offset_right = 380
pause_panel.offset_top = -300; pause_panel.offset_bottom = 300
canvas.add_child(pause_panel)
var vbox := VBoxContainer.new()
vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
vbox.add_theme_constant_override("separation", 12)
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 20)
margin.add_theme_constant_override("margin_right", 20)
margin.add_theme_constant_override("margin_top", 16)
margin.add_theme_constant_override("margin_bottom", 16)
pause_panel.add_child(margin)
margin.add_child(vbox)
var title := Label.new()
title.text = "ПАУЗА"
title.add_theme_font_size_override("font_size", 28)
title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.5))
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(title)
pause_image = TextureRect.new()
pause_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
pause_image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
pause_image.custom_minimum_size = Vector2(0, 380)
pause_image.size_flags_vertical = Control.SIZE_EXPAND_FILL
vbox.add_child(pause_image)
_pause_load_image()
var hbox := HBoxContainer.new()
hbox.alignment = BoxContainer.ALIGNMENT_CENTER
hbox.add_theme_constant_override("separation", 16)
vbox.add_child(hbox)
pause_toggle_btn = _pause_btn("Управление", _on_pause_toggle)
hbox.add_child(pause_toggle_btn)
hbox.add_child(_pause_btn("Продолжить", _toggle_pause_menu))
hbox.add_child(_pause_btn("Главное меню", func() -> void:
get_tree().paused = false
get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")
))
func _pause_btn(text: String, cb: Callable) -> Button:
var b := Button.new()
b.text = text
b.process_mode = Node.PROCESS_MODE_ALWAYS
b.custom_minimum_size = Vector2(160, 48)
b.add_theme_font_size_override("font_size", 17)
b.connect("pressed", cb)
return b
func _pause_load_image() -> void:
var path := _PAUSE_CRAFT_IMG if _pause_showing_craft else _PAUSE_CONTROLS_IMG
pause_image.texture = load(path) as Texture2D if ResourceLoader.exists(path) else null
func _on_pause_toggle() -> void:
_pause_showing_craft = not _pause_showing_craft
pause_toggle_btn.text = "Управление" if _pause_showing_craft else "Рецепты Крафта"
_pause_load_image()
func _toggle_pause_menu() -> void:
var opening := not pause_panel.visible
pause_panel.visible = opening
get_tree().paused = opening
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if opening else Input.MOUSE_MODE_CAPTURED
if opening:
_pause_showing_craft = true
pause_toggle_btn.text = "Управление"
_pause_load_image()
+20 -1
View File
@@ -18,7 +18,7 @@ func _build_ui() -> void:
add_child(bg)
var title := Label.new()
title.text = "KickSurvivors"
title.text = "CraftKick"
title.add_theme_font_size_override("font_size", 52)
title.add_theme_color_override("font_color", Color.WHITE)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
@@ -48,6 +48,25 @@ func _build_ui() -> void:
_build_settings_panel()
_build_difficulty_panel()
_add_studio_logo()
func _add_studio_logo() -> void:
const PATH := "res://assets/studio_logo.png"
var logo := TextureRect.new()
if ResourceLoader.exists(PATH):
logo.texture = load(PATH) as Texture2D
logo.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
logo.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
logo.anchor_left = 1.0
logo.anchor_right = 1.0
logo.anchor_top = 1.0
logo.anchor_bottom = 1.0
logo.offset_left = -200
logo.offset_right = -16
logo.offset_top = -208
logo.offset_bottom = -16
logo.modulate.a = 0.85
add_child(logo)
func _big_btn(text: String, width: float, cb: Callable) -> Button:
var b := _btn(text, cb)
+12 -2
View File
@@ -24,7 +24,7 @@ static var _list: Array[Dictionary] = [
"speed_threshold": 18.0,
},
{
"ingredients": ["leather", "metal_plate"],
"ingredients": ["leather", "iron"],
"result_scene": "res://scenes/PlateArmor.tscn",
"speed_threshold": 18.0,
},
@@ -34,10 +34,20 @@ static var _list: Array[Dictionary] = [
"speed_threshold": 18.0,
},
{
"ingredients": ["iron", "leather"],
"ingredients": ["iron", "rock"],
"result_scene": "res://scenes/IronShield.tscn",
"speed_threshold": 18.0,
},
{
"ingredients": ["iron", "Smith"],
"result_scene": "res://scenes/MetalPlate.tscn",
"speed_threshold": 0.1,
},
{
"ingredients": ["essence", "Enchanted_Table"],
"result_scene": "res://scenes/EnchantedSphere.tscn",
"speed_threshold": 0.1,
}
]
static func find(type_a: String, type_b: String, speed: float) -> Dictionary:
+17 -9
View File
@@ -21,6 +21,7 @@ var has_iron_shield: bool = false
var shield_tier: int = 0
var is_shielding: bool = false
var kick_timer: float = 0.0
var interact_timer: float = 0.0
var invincible_timer: float = 0.0
var is_alive: bool = true
var last_move_dir: Vector3 = Vector3.FORWARD
@@ -93,11 +94,6 @@ func _make_kick_arc_mesh() -> ArrayMesh:
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_E:
_try_interact()
func _physics_process(delta: float) -> void:
if not is_alive:
return
@@ -105,6 +101,9 @@ func _physics_process(delta: float) -> void:
_handle_movement(delta)
_handle_kick(delta)
_handle_iframes(delta)
interact_timer = max(0.0, interact_timer - delta)
if interact_timer <= 0.0:
_try_interact()
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and kick_timer <= 0.0:
_do_kick()
@@ -118,8 +117,16 @@ func _handle_movement(delta: float) -> void:
float(Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN)) -
float(Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP))
)
var is_moving: bool = abs(input_x) > 0.0 or abs(input_z) > 0.0
if is_moving:
if not anim_player.is_playing() or anim_player.current_animation != "walk":
anim_player.play("walk")
else:
if anim_player.current_animation == "walk":
anim_player.stop()
var cam := get_viewport().get_camera_3d()
if (abs(input_x) > 0.0 or abs(input_z) > 0.0) and cam != null:
if is_moving and cam != null:
var cam_fwd := -cam.global_transform.basis.z
cam_fwd.y = 0.0
cam_fwd = cam_fwd.normalized() if cam_fwd.length() > 0.01 else Vector3(0.0, 0.0, -1.0)
@@ -207,6 +214,7 @@ func _do_kick() -> void:
else:
force = 80.0
best.call("receive_kick", best_dir, force)
SFX.kick_player(get_parent())
FX.hit_spark(best.global_position + Vector3(0, 0.4, 0), get_parent())
_squish_effect()
@@ -262,16 +270,16 @@ func take_damage(amount: int, attacker_toughness: int = 0) -> void:
health = min(health - amount, max_health)
emit_signal("health_changed", health, max_health)
SFX.damage(get_parent())
_squish_effect()
if health <= 0:
_die()
func heal(amount: int) -> void:
if not is_alive:
return
health = min(health + amount, max_health)
emit_signal("health_changed", health, max_health)
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.1, 1.0, 0.35), 0.08)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.3)
func _die() -> void:
is_alive = false
+57
View File
@@ -0,0 +1,57 @@
class_name SFX
# Expected files in res://assets/sfx/:
# kick_player.ogg — player kicks something
# kick_enemy.ogg — enemy attacks
# merge.ogg — two objects merge
# damage.ogg — player takes damage
# ambient.ogg — looping background ambience
const _BASE := "res://assets/"
static func _play(name: String, parent: Node, volume_db: float = 0.0, pitch: float = 1.0) -> void:
var path := _BASE + name
if not ResourceLoader.exists(path):
return
var stream := load(path) as AudioStream
if stream == null:
return
var ogg = stream as AudioStreamOggVorbis
if ogg != null:
ogg.loop = false
var p := AudioStreamPlayer.new()
p.stream = stream
p.volume_db = volume_db
p.pitch_scale = pitch + randf_range(-0.06, 0.06)
p.bus = "Master"
parent.add_child(p)
p.play()
p.connect("finished", p.queue_free)
static func kick_player(parent: Node) -> void:
_play("kick_player.ogg", parent, -4.0)
static func kick_enemy(parent: Node) -> void:
_play("kick_enemy.ogg", parent, -6.0)
static func merge(parent: Node) -> void:
_play("merge.ogg", parent, -3.0)
static func damage(parent: Node) -> void:
_play("damage.ogg", parent, -2.0)
static func start_ambient(parent: Node) -> AudioStreamPlayer:
var path := _BASE + "ambient.ogg"
if not ResourceLoader.exists(path):
return null
var stream := load(path) as AudioStream
if stream == null:
return null
var p := AudioStreamPlayer.new()
p.stream = stream
p.volume_db = -14.0
p.bus = "Master"
p.autoplay = true
(stream as AudioStreamOggVorbis).loop = true
parent.add_child(p)
return p
+1
View File
@@ -0,0 +1 @@
uid://d2vxdhi2fmqhd