inital commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[folding]
|
||||
|
||||
node_unfolds=[NodePath("."), PackedStringArray("Collision")]
|
||||
resource_unfolds=[]
|
||||
nodes_folded=[]
|
||||
@@ -0,0 +1,5 @@
|
||||
[folding]
|
||||
|
||||
node_unfolds=[NodePath("."), PackedStringArray("Visibility")]
|
||||
resource_unfolds=[]
|
||||
nodes_folded=[]
|
||||
@@ -0,0 +1,5 @@
|
||||
[folding]
|
||||
|
||||
node_unfolds=[NodePath("."), PackedStringArray("Collision")]
|
||||
resource_unfolds=[]
|
||||
nodes_folded=[]
|
||||
@@ -0,0 +1,72 @@
|
||||
[docks]
|
||||
|
||||
dock_3_selected_tab_idx=0
|
||||
dock_4_selected_tab_idx=0
|
||||
dock_5_selected_tab_idx=0
|
||||
dock_floating={}
|
||||
dock_closed=[]
|
||||
dock_split_2=0
|
||||
dock_split_3=0
|
||||
dock_hsplit_1=0
|
||||
dock_hsplit_2=280
|
||||
dock_hsplit_3=-280
|
||||
dock_hsplit_4=0
|
||||
dock_9_selected_tab_idx=0
|
||||
dock_3="Scene,Import"
|
||||
dock_4="FileSystem,History"
|
||||
dock_5="Inspector,Signals,Groups"
|
||||
dock_9="Output,Debugger,Audio,Animation,Shader Editor,Search Results,AnimationTree,ResourcePreloader,ShaderFile,SpriteFrames,Theme,Polygon,TileSet,TileMap,Replication,GridMap"
|
||||
|
||||
[docks/FileSystem]
|
||||
|
||||
h_split_offset=480
|
||||
v_split_offset=0
|
||||
display_mode=0
|
||||
file_sort=0
|
||||
file_list_display_mode=1
|
||||
selected_paths=PackedStringArray("res://scenes/Player.tscn")
|
||||
uncollapsed_paths=PackedStringArray("Favorites", "res://", "res://scripts/", "res://scenes/")
|
||||
|
||||
[docks/History]
|
||||
|
||||
include_scene=true
|
||||
include_global=true
|
||||
|
||||
[EditorNode]
|
||||
|
||||
open_scenes=PackedStringArray("res://scenes/Player.tscn")
|
||||
current_scene="res://scenes/Player.tscn"
|
||||
bottom_panel_offsets={
|
||||
"Audio": -450,
|
||||
"Debugger": 0,
|
||||
"Output": 0
|
||||
}
|
||||
selected_default_debugger_tab_idx=1
|
||||
selected_main_editor_idx=1
|
||||
|
||||
[EditorWindow]
|
||||
|
||||
screen=0
|
||||
mode="maximized"
|
||||
position=Vector2i(0, 60)
|
||||
|
||||
[ScriptEditor]
|
||||
|
||||
open_scripts=["res://scripts/Enemy.gd", "res://scripts/Main.gd", "res://scripts/Player.gd"]
|
||||
selected_script="res://scripts/Enemy.gd"
|
||||
open_help=[]
|
||||
script_split_offset=400
|
||||
list_split_offset=0
|
||||
zoom_factor=1.0
|
||||
|
||||
[GameView]
|
||||
|
||||
floating_window_rect=Rect2i(854, 458, 1172, 806)
|
||||
floating_window_screen=0
|
||||
|
||||
[ShaderEditor]
|
||||
|
||||
open_shaders=[]
|
||||
split_offset=400
|
||||
selected_shader=""
|
||||
text_shader_zoom_factor=1.0
|
||||
@@ -0,0 +1,11 @@
|
||||
63f7b34db8d8cdea90c76aacccf841ec
|
||||
::res://::1776852182
|
||||
icon.svg::CompressedTexture2D::5982078435067271598::1776851681::1776852183::1::::<><><>0<>0<>ca0a886cd24b06b2f64ae5271fa97ce6<>res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex::
|
||||
::res://scenes/::1776851691
|
||||
Enemy.tscn::PackedScene::6409490066716950956::1776851687::0::1::::<><><>0<>0<><>::res://scripts/Enemy.gd
|
||||
Main.tscn::PackedScene::8897127817261996046::1776851691::0::1::::<><><>0<>0<><>::res://scripts/Main.gd
|
||||
Player.tscn::PackedScene::3474306994787601198::1776851684::0::1::::<><><>0<>0<><>::res://scripts/Player.gd
|
||||
::res://scripts/::1776852042
|
||||
Enemy.gd::GDScript::1423062711014319506::1776851960::0::1::::<>CharacterBody3D<><>0<>0<><>::
|
||||
Main.gd::GDScript::3051304906098389946::1776852042::0::1::::<>Node3D<><>0<>0<><>::
|
||||
Player.gd::GDScript::2700327509467176353::1776851717::0::1::::<>CharacterBody3D<><>0<>0<><>::
|
||||
@@ -0,0 +1,3 @@
|
||||
res://scripts/Enemy.gd
|
||||
res://scripts/Main.gd
|
||||
res://scripts/Player.gd
|
||||
@@ -0,0 +1,41 @@
|
||||
[res://scripts/Main.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 0,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 294,
|
||||
"scroll_position": 392.0,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
|
||||
[res://scripts/Enemy.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 15,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 192,
|
||||
"scroll_position": 184.0,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
|
||||
[res://scripts/Player.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 17,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 117,
|
||||
"scroll_position": 156.0,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
list=[]
|
||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
source_md5="e51b0fee176db4042f82efec3432fe85"
|
||||
dest_md5="39fbb8da890d40686d5f42136abbe9a9"
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128">
|
||||
<rect width="128" height="128" rx="16" fill="#1a1a2e"/>
|
||||
<circle cx="64" cy="64" r="30" fill="#0066cc"/>
|
||||
<text x="64" y="80" font-size="48" text-anchor="middle" fill="white">👟</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 264 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://crh07tiwwxj6u"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,20 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[animation]
|
||||
|
||||
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||
|
||||
[application]
|
||||
|
||||
config/name="KickSurvivors"
|
||||
run/main_scene="res://scenes/Main.tscn"
|
||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cxk2mn5oab341"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/Enemy.gd" id="1_enemy"]
|
||||
|
||||
[node name="Enemy" type="CharacterBody3D"]
|
||||
script = ExtResource("1_enemy")
|
||||
@@ -0,0 +1,11 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dyz3no6pcd567"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/Main.gd" id="1_main"]
|
||||
|
||||
[node name="Main" type="Node3D"]
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="SpawnTimer" type="Timer" parent="."]
|
||||
wait_time = 1.5
|
||||
one_shot = false
|
||||
autostart = false
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bpq8lf4mcvhw5"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/Player.gd" id="1_player"]
|
||||
|
||||
[node name="Player" type="CharacterBody3D"]
|
||||
script = ExtResource("1_player")
|
||||
@@ -0,0 +1,202 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
signal died(points: int)
|
||||
|
||||
enum State { CHASING, FLYING, STUNNED, DEAD }
|
||||
|
||||
var move_speed: float = 3.0
|
||||
var health: int = 30
|
||||
var damage_to_player: int = 8
|
||||
var score_value: int = 10
|
||||
var wall_damage_mult: float = 1.8
|
||||
var chain_factor: float = 0.65
|
||||
var stun_time: float = 0.5
|
||||
var base_scale: float = 1.0
|
||||
|
||||
var state: State = State.CHASING
|
||||
var fly_vel: Vector3 = Vector3.ZERO
|
||||
var stun_timer: float = 0.0
|
||||
var contact_timer: float = 0.0
|
||||
var target: Node3D = null
|
||||
|
||||
var mesh_node: MeshInstance3D
|
||||
var mat: StandardMaterial3D
|
||||
|
||||
var COLOR_CHASE = Color(1.0, 0.28, 0.18)
|
||||
var COLOR_FLY = Color(1.0, 0.85, 0.1)
|
||||
var COLOR_STUN = Color(0.55, 0.55, 0.65)
|
||||
const CONTACT_CD = 0.7
|
||||
const AIR_FRICTION = 0.86
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemies")
|
||||
_build_mesh()
|
||||
_build_collider()
|
||||
|
||||
func setup(type: String, wave: int) -> void:
|
||||
match type:
|
||||
"slime":
|
||||
move_speed = 2.8 + wave * 0.12
|
||||
health = 28 + wave * 4
|
||||
score_value = 10
|
||||
damage_to_player = 8
|
||||
"bat":
|
||||
move_speed = 5.5 + wave * 0.15
|
||||
health = 14 + wave * 2
|
||||
score_value = 15
|
||||
damage_to_player = 6
|
||||
base_scale = 0.7
|
||||
mesh_node.scale = Vector3(0.7, 0.7, 0.7)
|
||||
COLOR_CHASE = Color(0.6, 0.2, 0.8)
|
||||
mat.albedo_color = COLOR_CHASE
|
||||
"ogre":
|
||||
move_speed = 1.8 + wave * 0.08
|
||||
health = 80 + wave * 12
|
||||
score_value = 25
|
||||
damage_to_player = 18
|
||||
base_scale = 1.5
|
||||
mesh_node.scale = Vector3(1.5, 1.5, 1.5)
|
||||
COLOR_CHASE = Color(0.3, 0.7, 0.3)
|
||||
mat.albedo_color = COLOR_CHASE
|
||||
|
||||
func _build_mesh() -> void:
|
||||
mesh_node = MeshInstance3D.new()
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(0.85, 0.85, 0.85)
|
||||
mesh_node.mesh = box
|
||||
mesh_node.position.y = 0.425
|
||||
mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = COLOR_CHASE
|
||||
mat.roughness = 0.8
|
||||
mesh_node.material_override = mat
|
||||
add_child(mesh_node)
|
||||
|
||||
func _build_collider() -> void:
|
||||
var col := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = Vector3(0.85, 0.85, 0.85)
|
||||
col.shape = shape
|
||||
col.position.y = 0.425
|
||||
add_child(col)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
match state:
|
||||
State.CHASING: _chase(delta)
|
||||
State.FLYING: _fly(delta)
|
||||
State.STUNNED: _stun_tick(delta)
|
||||
State.DEAD: pass
|
||||
|
||||
func _chase(delta: float) -> void:
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
contact_timer = max(0.0, contact_timer - delta)
|
||||
|
||||
var diff := target.global_position - global_position
|
||||
diff.y = 0.0
|
||||
var dist := diff.length()
|
||||
|
||||
if dist < 1.0 and contact_timer <= 0.0:
|
||||
contact_timer = CONTACT_CD
|
||||
if target.has_method("take_damage"):
|
||||
target.take_damage(damage_to_player)
|
||||
|
||||
if dist > 0.05:
|
||||
var dir := diff.normalized()
|
||||
velocity.x = dir.x * move_speed
|
||||
velocity.z = dir.z * move_speed
|
||||
rotation.y = lerp_angle(rotation.y, atan2(dir.x, dir.z), 8.0 * delta)
|
||||
velocity.y = 0.0
|
||||
move_and_slide()
|
||||
|
||||
func _fly(delta: float) -> void:
|
||||
var speed_now := Vector2(fly_vel.x, fly_vel.z).length()
|
||||
velocity = fly_vel
|
||||
velocity.y = 0.0
|
||||
move_and_slide()
|
||||
|
||||
var hit_wall := false
|
||||
for i in get_slide_collision_count():
|
||||
var col := get_slide_collision(i)
|
||||
var col3d := col.get_collider() as Node3D
|
||||
if col3d == null:
|
||||
continue
|
||||
|
||||
if col3d.has_meta("is_wall"):
|
||||
var dmg := int(speed_now * wall_damage_mult)
|
||||
_take_hit(dmg)
|
||||
_wall_impact_effect()
|
||||
fly_vel = Vector3.ZERO
|
||||
velocity = Vector3.ZERO
|
||||
_enter_stun()
|
||||
hit_wall = true
|
||||
break
|
||||
elif col3d.is_in_group("enemies") and col3d != self:
|
||||
var chain_dir := col3d.global_position - global_position
|
||||
chain_dir.y = 0.0
|
||||
if chain_dir.length() > 0.01:
|
||||
col3d.call("receive_kick", chain_dir.normalized(), speed_now * chain_factor)
|
||||
elif col3d.is_in_group("player"):
|
||||
col3d.call("take_damage", int(speed_now * 0.6))
|
||||
|
||||
if not hit_wall:
|
||||
fly_vel = velocity
|
||||
fly_vel.y = 0.0
|
||||
fly_vel *= pow(AIR_FRICTION, delta * 60.0)
|
||||
if Vector2(fly_vel.x, fly_vel.z).length() < 0.4:
|
||||
_enter_chase()
|
||||
|
||||
# Spin while airborne
|
||||
mesh_node.rotation.y += delta * 10.0
|
||||
|
||||
func _stun_tick(delta: float) -> void:
|
||||
velocity = Vector3.ZERO
|
||||
stun_timer -= delta
|
||||
if stun_timer <= 0.0:
|
||||
_enter_chase()
|
||||
|
||||
func receive_kick(direction: Vector3, force: float) -> void:
|
||||
if state == State.DEAD:
|
||||
return
|
||||
fly_vel = direction * force
|
||||
fly_vel.y = 0.0
|
||||
state = State.FLYING
|
||||
mat.albedo_color = COLOR_FLY
|
||||
var tw := create_tween()
|
||||
tw.tween_property(mesh_node, "scale:y", base_scale * 0.35, 0.06)
|
||||
tw.tween_property(mesh_node, "scale:y", base_scale, 0.18)
|
||||
|
||||
func _enter_stun() -> void:
|
||||
state = State.STUNNED
|
||||
stun_timer = stun_time
|
||||
mat.albedo_color = COLOR_STUN
|
||||
var bs := base_scale
|
||||
var tw := create_tween()
|
||||
tw.tween_property(mesh_node, "scale", Vector3(bs * 1.6, bs * 0.25, bs * 1.6), 0.07)
|
||||
tw.tween_property(mesh_node, "scale", Vector3(bs, bs, bs), 0.22)
|
||||
|
||||
func _enter_chase() -> void:
|
||||
state = State.CHASING
|
||||
mat.albedo_color = COLOR_CHASE
|
||||
|
||||
func _take_hit(dmg: int) -> void:
|
||||
if state == State.DEAD:
|
||||
return
|
||||
health -= dmg
|
||||
if health <= 0:
|
||||
_die()
|
||||
|
||||
func _wall_impact_effect() -> void:
|
||||
var tw := create_tween()
|
||||
tw.tween_property(mat, "albedo_color", Color.WHITE, 0.04)
|
||||
tw.tween_property(mat, "albedo_color", COLOR_STUN, 0.12)
|
||||
|
||||
func _die() -> void:
|
||||
if state == State.DEAD:
|
||||
return
|
||||
state = State.DEAD
|
||||
set_physics_process(false)
|
||||
emit_signal("died", score_value)
|
||||
var tw := create_tween()
|
||||
tw.tween_property(self, "scale", Vector3(2.0, 0.05, 2.0), 0.18)
|
||||
tw.tween_property(self, "scale", Vector3(0.0, 0.0, 0.0), 0.1)
|
||||
tw.tween_callback(queue_free)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ujlvt8u71hg1
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
extends Node3D
|
||||
|
||||
const PLAYER_SCENE := preload("res://scenes/Player.tscn")
|
||||
const ENEMY_SCENE := preload("res://scenes/Enemy.tscn")
|
||||
|
||||
const ARENA := 14.0
|
||||
const WALL_T := 1.2
|
||||
const CAM_DIST := 8.0
|
||||
const MOUSE_SENS := 0.18
|
||||
const PITCH_MIN := 5.0
|
||||
const PITCH_MAX := 70.0
|
||||
|
||||
var cam_yaw: float = 0.0
|
||||
var cam_pitch: float = 28.0
|
||||
|
||||
@onready var spawn_timer: Timer = $SpawnTimer
|
||||
|
||||
var player: CharacterBody3D
|
||||
var camera: Camera3D
|
||||
var wave: int = 1
|
||||
var score: int = 0
|
||||
var kills: int = 0
|
||||
var kills_for_next: int = 10
|
||||
var game_active: bool = false
|
||||
var upgrading: bool = false
|
||||
|
||||
# UI nodes
|
||||
var canvas: CanvasLayer
|
||||
var score_label: Label
|
||||
var wave_label: Label
|
||||
var hp_bar: ColorRect
|
||||
var hp_bar_bg: ColorRect
|
||||
var progress_bar: ColorRect
|
||||
var progress_bg: ColorRect
|
||||
var upgrade_panel: Panel
|
||||
var gameover_panel: Panel
|
||||
|
||||
func _ready() -> void:
|
||||
_create_environment()
|
||||
_create_arena()
|
||||
_create_camera()
|
||||
_create_ui()
|
||||
_spawn_player()
|
||||
_start_game()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
var motion := event as InputEventMouseMotion
|
||||
if motion != null and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
cam_yaw -= motion.relative.x * MOUSE_SENS
|
||||
cam_pitch -= motion.relative.y * MOUSE_SENS
|
||||
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
|
||||
else:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
# ─── Environment ──────────────────────────────────────────────────────────────
|
||||
|
||||
func _create_environment() -> void:
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color(0.04, 0.04, 0.08)
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(0.35, 0.35, 0.5)
|
||||
env.ambient_light_energy = 0.6
|
||||
var we := WorldEnvironment.new()
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-55, -25, 0)
|
||||
sun.light_energy = 1.8
|
||||
sun.shadow_enabled = true
|
||||
add_child(sun)
|
||||
|
||||
# ─── Arena ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _create_arena() -> void:
|
||||
var floor_mat := StandardMaterial3D.new()
|
||||
floor_mat.albedo_color = Color(0.12, 0.12, 0.18)
|
||||
floor_mat.roughness = 1.0
|
||||
|
||||
# Floor mesh (visual only)
|
||||
var fm := MeshInstance3D.new()
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(ARENA * 2, ARENA * 2)
|
||||
plane.subdivide_width = 8
|
||||
plane.subdivide_depth = 8
|
||||
fm.mesh = plane
|
||||
fm.material_override = floor_mat
|
||||
add_child(fm)
|
||||
|
||||
# Floor collider
|
||||
var fb := StaticBody3D.new()
|
||||
var fc := CollisionShape3D.new()
|
||||
var fs := BoxShape3D.new()
|
||||
fs.size = Vector3(ARENA * 2, 0.2, ARENA * 2)
|
||||
fc.shape = fs
|
||||
fb.position.y = -0.1
|
||||
fb.add_child(fc)
|
||||
add_child(fb)
|
||||
|
||||
# Grid lines on floor
|
||||
_draw_grid()
|
||||
|
||||
# Four walls
|
||||
var wall_mat := StandardMaterial3D.new()
|
||||
wall_mat.albedo_color = Color(0.28, 0.28, 0.42)
|
||||
wall_mat.roughness = 0.9
|
||||
wall_mat.metallic = 0.1
|
||||
|
||||
_make_wall(Vector3(0, 0.5, -(ARENA + WALL_T * 0.5)),
|
||||
Vector3(ARENA * 2 + WALL_T * 2, 1.0, WALL_T), wall_mat)
|
||||
_make_wall(Vector3(0, 0.5, (ARENA + WALL_T * 0.5)),
|
||||
Vector3(ARENA * 2 + WALL_T * 2, 1.0, WALL_T), wall_mat)
|
||||
_make_wall(Vector3(-(ARENA + WALL_T * 0.5), 0.5, 0),
|
||||
Vector3(WALL_T, 1.0, ARENA * 2), wall_mat)
|
||||
_make_wall(Vector3( (ARENA + WALL_T * 0.5), 0.5, 0),
|
||||
Vector3(WALL_T, 1.0, ARENA * 2), wall_mat)
|
||||
|
||||
func _make_wall(pos: Vector3, size: Vector3, mat: StandardMaterial3D) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.position = pos
|
||||
body.set_meta("is_wall", true)
|
||||
|
||||
var col := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = size
|
||||
col.shape = shape
|
||||
body.add_child(col)
|
||||
|
||||
var msh := MeshInstance3D.new()
|
||||
var box := BoxMesh.new()
|
||||
box.size = size
|
||||
msh.mesh = box
|
||||
msh.material_override = mat
|
||||
body.add_child(msh)
|
||||
|
||||
add_child(body)
|
||||
|
||||
func _draw_grid() -> void:
|
||||
# Subtle grid as thin quads
|
||||
var grid_mat := StandardMaterial3D.new()
|
||||
grid_mat.albedo_color = Color(0.2, 0.2, 0.3, 0.5)
|
||||
grid_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
grid_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
var step := 4.0
|
||||
var n := int(ARENA / step)
|
||||
for i in range(-n, n + 1):
|
||||
for axis in [0, 1]:
|
||||
var msh := MeshInstance3D.new()
|
||||
var box := BoxMesh.new()
|
||||
if axis == 0:
|
||||
box.size = Vector3(0.05, 0.01, ARENA * 2)
|
||||
msh.position = Vector3(i * step, 0.005, 0)
|
||||
else:
|
||||
box.size = Vector3(ARENA * 2, 0.01, 0.05)
|
||||
msh.position = Vector3(0, 0.005, i * step)
|
||||
msh.mesh = box
|
||||
msh.material_override = grid_mat
|
||||
add_child(msh)
|
||||
|
||||
# ─── Camera ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func _create_camera() -> void:
|
||||
camera = Camera3D.new()
|
||||
camera.fov = 70.0
|
||||
add_child(camera)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if is_instance_valid(player):
|
||||
var yaw_r: float = deg_to_rad(cam_yaw)
|
||||
var pitch_r: float = deg_to_rad(cam_pitch)
|
||||
var offset := Vector3(
|
||||
sin(yaw_r) * cos(pitch_r) * CAM_DIST,
|
||||
sin(pitch_r) * CAM_DIST,
|
||||
cos(yaw_r) * cos(pitch_r) * CAM_DIST
|
||||
)
|
||||
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)
|
||||
|
||||
# ─── Player ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func _spawn_player() -> void:
|
||||
player = PLAYER_SCENE.instantiate() as CharacterBody3D
|
||||
player.position = Vector3(0, 0, 0)
|
||||
player.connect("died", _on_player_died)
|
||||
player.connect("health_changed", _on_health_changed)
|
||||
add_child(player)
|
||||
|
||||
# ─── Game flow ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _start_game() -> void:
|
||||
game_active = true
|
||||
wave = 1
|
||||
score = 0
|
||||
kills = 0
|
||||
kills_for_next = 10
|
||||
_update_labels()
|
||||
spawn_timer.wait_time = 1.4
|
||||
spawn_timer.connect("timeout", _on_spawn_timer)
|
||||
spawn_timer.start()
|
||||
|
||||
func _on_spawn_timer() -> void:
|
||||
if not game_active or upgrading:
|
||||
return
|
||||
_spawn_enemy()
|
||||
spawn_timer.wait_time = max(0.25, 1.4 - wave * 0.07)
|
||||
|
||||
func _spawn_enemy() -> void:
|
||||
var enemy := ENEMY_SCENE.instantiate() as CharacterBody3D
|
||||
add_child(enemy)
|
||||
|
||||
# Pick type based on wave
|
||||
var pool: Array[String] = ["slime"]
|
||||
if wave >= 4: pool.append("bat")
|
||||
if wave >= 7: pool.append("ogre")
|
||||
var type: String = pool[randi() % pool.size()]
|
||||
enemy.setup(type, wave)
|
||||
enemy.target = player
|
||||
enemy.connect("died", _on_enemy_died)
|
||||
|
||||
# Spawn at random edge
|
||||
var side := randi() % 4
|
||||
var r := randf_range(-(ARENA - 1.0), ARENA - 1.0)
|
||||
match side:
|
||||
0: enemy.position = Vector3(r, 0, -(ARENA - 0.5))
|
||||
1: enemy.position = Vector3(r, 0, (ARENA - 0.5))
|
||||
2: enemy.position = Vector3(-(ARENA - 0.5), 0, r)
|
||||
3: enemy.position = Vector3( (ARENA - 0.5), 0, r)
|
||||
|
||||
func _on_enemy_died(points: int) -> void:
|
||||
score += points
|
||||
kills += 1
|
||||
_update_labels()
|
||||
_update_progress()
|
||||
|
||||
if kills >= kills_for_next:
|
||||
kills = 0
|
||||
kills_for_next = int(kills_for_next * 1.6)
|
||||
wave += 1
|
||||
_show_upgrade()
|
||||
|
||||
func _on_player_died() -> void:
|
||||
game_active = false
|
||||
spawn_timer.stop()
|
||||
_show_gameover()
|
||||
|
||||
func _on_health_changed(cur: int, mx: int) -> void:
|
||||
hp_bar.size.x = 200.0 * float(cur) / float(mx)
|
||||
|
||||
# ─── Upgrades ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const UPGRADES := [
|
||||
{"id": "kick_force", "name": "💥 Stronger Kick", "desc": "Enemies fly farther"},
|
||||
{"id": "kick_range", "name": "🌐 Wider Kick", "desc": "Bigger kick area"},
|
||||
{"id": "kick_cooldown", "name": "⚡ Faster Kick", "desc": "Kick more often"},
|
||||
{"id": "move_speed", "name": "💨 Fleet Foot", "desc": "Move faster"},
|
||||
{"id": "max_health", "name": "❤️ Vitality", "desc": "+30 max HP"},
|
||||
]
|
||||
|
||||
func _show_upgrade() -> void:
|
||||
upgrading = true
|
||||
get_tree().paused = true
|
||||
upgrade_panel.visible = true
|
||||
|
||||
# Pick 3 random upgrades
|
||||
var pool: Array = UPGRADES.duplicate()
|
||||
pool.shuffle()
|
||||
var choices: Array = pool.slice(0, 3)
|
||||
|
||||
for i in range(3):
|
||||
var btn := upgrade_panel.get_node("VBox/Btn%d" % i) as Button
|
||||
if i < choices.size():
|
||||
var upg: Dictionary = choices[i]
|
||||
btn.text = "%s\n%s" % [upg["name"], upg["desc"]]
|
||||
btn.visible = true
|
||||
# Disconnect old signals
|
||||
for conn in btn.get_signal_connection_list("pressed"):
|
||||
btn.disconnect("pressed", conn["callable"])
|
||||
var uid: String = upg["id"]
|
||||
btn.connect("pressed", _pick_upgrade.bind(uid))
|
||||
else:
|
||||
btn.visible = false
|
||||
|
||||
func _pick_upgrade(id: String) -> void:
|
||||
player.apply_upgrade(id)
|
||||
upgrade_panel.visible = false
|
||||
get_tree().paused = false
|
||||
upgrading = false
|
||||
_update_labels()
|
||||
|
||||
func _show_gameover() -> void:
|
||||
gameover_panel.visible = true
|
||||
var lbl := gameover_panel.get_node("VBox/ScoreLabel") as Label
|
||||
lbl.text = "Score: %d\nWave: %d" % [score, wave]
|
||||
|
||||
func _restart() -> void:
|
||||
get_tree().paused = false
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
# ─── UI ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _create_ui() -> void:
|
||||
canvas = CanvasLayer.new()
|
||||
canvas.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
add_child(canvas)
|
||||
|
||||
_make_hud()
|
||||
_make_upgrade_panel()
|
||||
_make_gameover_panel()
|
||||
|
||||
func _make_hud() -> void:
|
||||
# Score
|
||||
score_label = _label(Vector2(12, 10), "Score: 0", 22)
|
||||
# Wave
|
||||
wave_label = _label(Vector2(12, 38), "Wave: 1", 22)
|
||||
|
||||
# HP bar
|
||||
_label(Vector2(12, 68), "HP", 16)
|
||||
hp_bar_bg = _crect(Vector2(12, 88), Vector2(200, 16), Color(0.25, 0.04, 0.04))
|
||||
hp_bar = _crect(Vector2(12, 88), Vector2(200, 16), Color(0.9, 0.15, 0.15))
|
||||
|
||||
# Kill progress toward next upgrade
|
||||
_label(Vector2(12, 110), "Next upgrade", 16)
|
||||
progress_bg = _crect(Vector2(12, 130), Vector2(200, 10), Color(0.1, 0.1, 0.25))
|
||||
progress_bar = _crect(Vector2(12, 130), Vector2(0, 10), Color(0.4, 0.8, 1.0))
|
||||
|
||||
func _make_upgrade_panel() -> void:
|
||||
upgrade_panel = Panel.new()
|
||||
upgrade_panel.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
upgrade_panel.visible = false
|
||||
canvas.add_child(upgrade_panel)
|
||||
|
||||
# Center with explicit anchors (420x300)
|
||||
upgrade_panel.anchor_left = 0.5
|
||||
upgrade_panel.anchor_right = 0.5
|
||||
upgrade_panel.anchor_top = 0.5
|
||||
upgrade_panel.anchor_bottom = 0.5
|
||||
upgrade_panel.offset_left = -210.0
|
||||
upgrade_panel.offset_right = 210.0
|
||||
upgrade_panel.offset_top = -150.0
|
||||
upgrade_panel.offset_bottom = 150.0
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "VBox"
|
||||
vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.add_theme_constant_override("separation", 12)
|
||||
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
upgrade_panel.add_child(vbox)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "LEVEL UP! Choose an upgrade:"
|
||||
title.add_theme_font_size_override("font_size", 20)
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(title)
|
||||
|
||||
for i in range(3):
|
||||
var btn := Button.new()
|
||||
btn.name = "Btn%d" % i
|
||||
btn.custom_minimum_size = Vector2(380, 60)
|
||||
btn.add_theme_font_size_override("font_size", 16)
|
||||
btn.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
vbox.add_child(btn)
|
||||
|
||||
func _make_gameover_panel() -> void:
|
||||
gameover_panel = Panel.new()
|
||||
gameover_panel.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
gameover_panel.visible = false
|
||||
canvas.add_child(gameover_panel)
|
||||
|
||||
# Center with explicit anchors (360x240)
|
||||
gameover_panel.anchor_left = 0.5
|
||||
gameover_panel.anchor_right = 0.5
|
||||
gameover_panel.anchor_top = 0.5
|
||||
gameover_panel.anchor_bottom = 0.5
|
||||
gameover_panel.offset_left = -180.0
|
||||
gameover_panel.offset_right = 180.0
|
||||
gameover_panel.offset_top = -120.0
|
||||
gameover_panel.offset_bottom = 120.0
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "VBox"
|
||||
vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
vbox.add_theme_constant_override("separation", 16)
|
||||
gameover_panel.add_child(vbox)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "GAME OVER"
|
||||
title.add_theme_font_size_override("font_size", 32)
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(title)
|
||||
|
||||
var score_lbl := Label.new()
|
||||
score_lbl.name = "ScoreLabel"
|
||||
score_lbl.add_theme_font_size_override("font_size", 20)
|
||||
score_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(score_lbl)
|
||||
|
||||
var restart_btn := Button.new()
|
||||
restart_btn.text = "Play Again"
|
||||
restart_btn.add_theme_font_size_override("font_size", 18)
|
||||
restart_btn.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
restart_btn.connect("pressed", _restart)
|
||||
vbox.add_child(restart_btn)
|
||||
|
||||
# ─── UI helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _label(pos: Vector2, text: String, size: int) -> Label:
|
||||
var lbl := Label.new()
|
||||
lbl.position = pos
|
||||
lbl.text = text
|
||||
lbl.add_theme_font_size_override("font_size", size)
|
||||
lbl.add_theme_color_override("font_color", Color.WHITE)
|
||||
lbl.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
|
||||
lbl.add_theme_constant_override("shadow_offset_x", 2)
|
||||
lbl.add_theme_constant_override("shadow_offset_y", 2)
|
||||
canvas.add_child(lbl)
|
||||
return lbl
|
||||
|
||||
func _crect(pos: Vector2, sz: Vector2, col: Color) -> ColorRect:
|
||||
var r := ColorRect.new()
|
||||
r.position = pos
|
||||
r.size = sz
|
||||
r.color = col
|
||||
canvas.add_child(r)
|
||||
return r
|
||||
|
||||
func _update_labels() -> void:
|
||||
score_label.text = "Score: %d" % score
|
||||
wave_label.text = "Wave: %d" % wave
|
||||
|
||||
func _update_progress() -> void:
|
||||
var t := float(kills) / float(kills_for_next)
|
||||
progress_bar.size.x = 200.0 * t
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjqcstgcoedos
|
||||
@@ -0,0 +1,231 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
signal died
|
||||
signal health_changed(current: int, maximum: int)
|
||||
|
||||
@export var move_speed: float = 7.0
|
||||
@export var kick_range: float = 3.5
|
||||
@export var kick_force: float = 22.0
|
||||
@export var kick_cooldown: float = 0.6
|
||||
@export var kick_angle: float = 120.0
|
||||
@export var max_health: int = 100
|
||||
|
||||
var health: int = max_health
|
||||
var kick_timer: float = 0.0
|
||||
var invincible_timer: float = 0.0
|
||||
var is_alive: bool = true
|
||||
var last_move_dir: Vector3 = Vector3.FORWARD
|
||||
|
||||
var mesh_node: MeshInstance3D
|
||||
var player_mat: StandardMaterial3D
|
||||
var indicator_node: MeshInstance3D
|
||||
var indicator_mat: StandardMaterial3D
|
||||
|
||||
const IFRAMES_DURATION := 0.6
|
||||
const BASE_COLOR := Color(0.2, 0.55, 1.0)
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player")
|
||||
_build_visuals()
|
||||
_build_collider()
|
||||
|
||||
func _build_visuals() -> void:
|
||||
mesh_node = MeshInstance3D.new()
|
||||
var capsule := CapsuleMesh.new()
|
||||
capsule.radius = 0.4
|
||||
capsule.height = 1.0
|
||||
mesh_node.mesh = capsule
|
||||
mesh_node.position.y = 0.5
|
||||
player_mat = StandardMaterial3D.new()
|
||||
player_mat.albedo_color = BASE_COLOR
|
||||
player_mat.roughness = 0.6
|
||||
player_mat.metallic = 0.2
|
||||
mesh_node.material_override = player_mat
|
||||
add_child(mesh_node)
|
||||
|
||||
# Kick arc indicator (sector/fan mesh on floor)
|
||||
indicator_node = MeshInstance3D.new()
|
||||
indicator_node.mesh = _make_kick_arc_mesh()
|
||||
indicator_node.position.y = 0.02
|
||||
indicator_mat = StandardMaterial3D.new()
|
||||
indicator_mat.albedo_color = Color(1.0, 0.85, 0.1, 0.2)
|
||||
indicator_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
indicator_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
indicator_mat.no_depth_test = true
|
||||
indicator_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
indicator_node.material_override = indicator_mat
|
||||
add_child(indicator_node)
|
||||
|
||||
func _make_kick_arc_mesh() -> ArrayMesh:
|
||||
var verts := PackedVector3Array()
|
||||
var half_rad: float = deg_to_rad(kick_angle * 0.5)
|
||||
const SEGS := 24
|
||||
verts.append(Vector3(0.0, 0.0, 0.0))
|
||||
for i in range(SEGS + 1):
|
||||
var t: float = float(i) / float(SEGS)
|
||||
var a: float = lerpf(-half_rad, half_rad, t)
|
||||
verts.append(Vector3(sin(a) * kick_range, 0.0, -cos(a) * kick_range))
|
||||
|
||||
var indices := PackedInt32Array()
|
||||
for i in range(SEGS):
|
||||
indices.append(0)
|
||||
indices.append(i + 1)
|
||||
indices.append(i + 2)
|
||||
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
func _build_collider() -> void:
|
||||
var col := CollisionShape3D.new()
|
||||
var shape := CapsuleShape3D.new()
|
||||
shape.radius = 0.4
|
||||
shape.height = 1.0
|
||||
col.shape = shape
|
||||
col.position.y = 0.5
|
||||
add_child(col)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not is_alive:
|
||||
return
|
||||
|
||||
_handle_movement(delta)
|
||||
_handle_kick(delta)
|
||||
_handle_iframes(delta)
|
||||
|
||||
if Input.is_action_just_pressed("ui_accept") and kick_timer <= 0.0:
|
||||
_do_kick()
|
||||
|
||||
func _handle_movement(delta: float) -> void:
|
||||
var input_x: float = (
|
||||
float(Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT)) -
|
||||
float(Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT))
|
||||
)
|
||||
var input_z: float = (
|
||||
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))
|
||||
)
|
||||
|
||||
# Camera-relative movement: camera is independent (mouse-controlled), no feedback loop
|
||||
var cam := get_viewport().get_camera_3d()
|
||||
if (abs(input_x) > 0.0 or abs(input_z) > 0.0) and cam != null:
|
||||
var cam_fwd := -cam.global_transform.basis.z
|
||||
cam_fwd.y = 0.0
|
||||
if cam_fwd.length() > 0.01:
|
||||
cam_fwd = cam_fwd.normalized()
|
||||
else:
|
||||
cam_fwd = Vector3(0.0, 0.0, -1.0)
|
||||
|
||||
var cam_right := cam.global_transform.basis.x
|
||||
cam_right.y = 0.0
|
||||
if cam_right.length() > 0.01:
|
||||
cam_right = cam_right.normalized()
|
||||
else:
|
||||
cam_right = Vector3(1.0, 0.0, 0.0)
|
||||
|
||||
var move := cam_fwd * (-input_z) + cam_right * input_x
|
||||
if move.length() > 0.01:
|
||||
move = move.normalized()
|
||||
velocity.x = move.x * move_speed
|
||||
velocity.z = move.z * move_speed
|
||||
last_move_dir = move
|
||||
var target_y: float = atan2(-move.x, -move.z)
|
||||
rotation.y = lerp_angle(rotation.y, target_y, 16.0 * delta)
|
||||
else:
|
||||
velocity.x = move_toward(velocity.x, 0.0, move_speed * 12.0 * delta)
|
||||
velocity.z = move_toward(velocity.z, 0.0, move_speed * 12.0 * delta)
|
||||
|
||||
velocity.y = 0.0
|
||||
move_and_slide()
|
||||
|
||||
func _handle_kick(delta: float) -> void:
|
||||
kick_timer = max(0.0, kick_timer - delta)
|
||||
var t: float = clamp(1.0 - kick_timer / kick_cooldown, 0.0, 1.0)
|
||||
indicator_mat.albedo_color.a = 0.05 + t * 0.3
|
||||
|
||||
func _handle_iframes(delta: float) -> void:
|
||||
if invincible_timer > 0.0:
|
||||
invincible_timer -= delta
|
||||
if invincible_timer > 0.0:
|
||||
mesh_node.visible = fmod(invincible_timer * 10.0, 1.0) > 0.5
|
||||
else:
|
||||
mesh_node.visible = true
|
||||
|
||||
func _do_kick() -> void:
|
||||
kick_timer = kick_cooldown
|
||||
|
||||
var forward := -global_transform.basis.z
|
||||
forward.y = 0.0
|
||||
if forward.length() > 0.01:
|
||||
forward = forward.normalized()
|
||||
else:
|
||||
forward = Vector3(0, 0, -1)
|
||||
|
||||
var half_cos: float = cos(deg_to_rad(kick_angle * 0.5))
|
||||
|
||||
var enemies := get_tree().get_nodes_in_group("enemies")
|
||||
var kicked_any := false
|
||||
for e in enemies:
|
||||
if not is_instance_valid(e):
|
||||
continue
|
||||
var en := e as Node3D
|
||||
if en == null:
|
||||
continue
|
||||
var diff := en.global_position - global_position
|
||||
diff.y = 0.0
|
||||
var dist := diff.length()
|
||||
if dist < 0.1 or dist > kick_range:
|
||||
continue
|
||||
var dir_to_enemy := diff / dist
|
||||
if dir_to_enemy.dot(forward) >= half_cos:
|
||||
en.call("receive_kick", dir_to_enemy, kick_force)
|
||||
kicked_any = true
|
||||
|
||||
if kicked_any:
|
||||
_squish_effect()
|
||||
|
||||
func _squish_effect() -> void:
|
||||
var tw := create_tween()
|
||||
tw.tween_property(mesh_node, "scale", Vector3(1.3, 0.55, 1.3), 0.07)
|
||||
tw.tween_property(mesh_node, "scale", Vector3(1.0, 1.0, 1.0), 0.18)
|
||||
|
||||
func take_damage(amount: int) -> void:
|
||||
if not is_alive or invincible_timer > 0.0:
|
||||
return
|
||||
invincible_timer = IFRAMES_DURATION
|
||||
health = max(0, health - amount)
|
||||
emit_signal("health_changed", health, max_health)
|
||||
|
||||
var tw := create_tween()
|
||||
tw.tween_property(player_mat, "albedo_color", Color.RED, 0.08)
|
||||
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.25)
|
||||
|
||||
if health <= 0:
|
||||
_die()
|
||||
|
||||
func _die() -> void:
|
||||
is_alive = false
|
||||
emit_signal("died")
|
||||
var tw := create_tween()
|
||||
tw.tween_property(self, "scale", Vector3(2.0, 0.0, 2.0), 0.35)
|
||||
|
||||
func apply_upgrade(id: String) -> void:
|
||||
match id:
|
||||
"kick_force":
|
||||
kick_force += 6.0
|
||||
"kick_range":
|
||||
kick_range += 0.7
|
||||
indicator_node.mesh = _make_kick_arc_mesh()
|
||||
"kick_cooldown":
|
||||
kick_cooldown = max(0.12, kick_cooldown - 0.09)
|
||||
"move_speed":
|
||||
move_speed += 1.2
|
||||
"max_health":
|
||||
max_health += 30
|
||||
health = min(health + 30, max_health)
|
||||
emit_signal("health_changed", health, max_health)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user