diff --git a/scenes/Boulder.tscn b/scenes/Boulder.tscn new file mode 100644 index 0000000..83c9f4d --- /dev/null +++ b/scenes/Boulder.tscn @@ -0,0 +1,27 @@ +[gd_scene format=3 uid="uid://bavtajgxrkrc1"] + +[ext_resource type="Script" path="res://scripts/Boulder.gd" id="1_boulder"] + +[sub_resource type="SphereMesh" id="SphereMesh_1"] +radius = 0.45 +height = 0.9 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(0.32, 0.28, 0.22, 1) +roughness = 1.0 +metallic = 0.08 + +[sub_resource type="SphereShape3D" id="SphereShape3D_1"] +radius = 0.45 + +[node name="Boulder" type="CharacterBody3D"] +script = ExtResource("1_boulder") + +[node name="BoulderMesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.45, 0) +mesh = SubResource("SphereMesh_1") +material_override = SubResource("StandardMaterial3D_1") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.45, 0) +shape = SubResource("SphereShape3D_1") diff --git a/scenes/Leather.tscn b/scenes/Leather.tscn new file mode 100644 index 0000000..fa96795 --- /dev/null +++ b/scenes/Leather.tscn @@ -0,0 +1,25 @@ +[gd_scene format=3 uid="uid://d4leath8bvq2r"] + +[ext_resource type="Script" path="res://scripts/Leather.gd" id="1_leather"] + +[sub_resource type="BoxMesh" id="BoxMesh_1"] +size = Vector3(0.5, 0.06, 0.35) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(0.62, 0.38, 0.20, 1) +roughness = 0.85 + +[sub_resource type="BoxShape3D" id="BoxShape3D_1"] +size = Vector3(0.5, 0.06, 0.35) + +[node name="Leather" type="CharacterBody3D"] +script = ExtResource("1_leather") + +[node name="LeatherMesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0) +mesh = SubResource("BoxMesh_1") +material_override = SubResource("StandardMaterial3D_1") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0) +shape = SubResource("BoxShape3D_1") diff --git a/scenes/LeatherBoots.tscn b/scenes/LeatherBoots.tscn new file mode 100644 index 0000000..9bf81b0 --- /dev/null +++ b/scenes/LeatherBoots.tscn @@ -0,0 +1,27 @@ +[gd_scene format=3 uid="uid://cbvs3rvwslsd2"] + +[ext_resource type="Script" path="res://scripts/LeatherBoots.gd" id="1_boots"] + +[sub_resource type="BoxMesh" id="BoxMesh_1"] +size = Vector3(0.4, 0.2, 0.55) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(0.45, 0.25, 0.10, 1) +roughness = 0.8 +metallic = 0.1 + +[node name="LeatherBoots" type="Node3D"] +script = ExtResource("1_boots") + +[node name="BootsMesh" type="MeshInstance3D" parent="."] +mesh = SubResource("BoxMesh_1") +material_override = SubResource("StandardMaterial3D_1") + +[node name="Tooltip" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0) +billboard = 1 +double_sided = true +text = "[E] Leather Boots ++10 Move Speed" +font_size = 32 +outline_size = 6 diff --git a/scenes/Stick.tscn b/scenes/Stick.tscn new file mode 100644 index 0000000..b27bae2 --- /dev/null +++ b/scenes/Stick.tscn @@ -0,0 +1,28 @@ +[gd_scene format=3 uid="uid://c3sticlaxmn1p"] + +[ext_resource type="Script" path="res://scripts/Stick.gd" id="1_stick"] + +[sub_resource type="CylinderMesh" id="CylinderMesh_1"] +top_radius = 0.06 +bottom_radius = 0.06 +height = 0.65 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(0.55, 0.38, 0.18, 1) +roughness = 0.95 + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] +radius = 0.07 +height = 0.65 + +[node name="Stick" type="CharacterBody3D"] +script = ExtResource("1_stick") + +[node name="StickMesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.32, 0) +mesh = SubResource("CylinderMesh_1") +material_override = SubResource("StandardMaterial3D_1") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.32, 0) +shape = SubResource("CapsuleShape3D_1") diff --git a/scripts/Boulder.gd b/scripts/Boulder.gd new file mode 100644 index 0000000..16442f1 --- /dev/null +++ b/scripts/Boulder.gd @@ -0,0 +1,113 @@ +extends CharacterBody3D + +signal destroyed + +enum State { IDLE, FLYING } + +const AIR_FRICTION := 0.88 +const MIN_SPEED := 0.5 +const WALL_BOUNCE := 0.4 +const WALL_SELF_DMG := 0.4 + +var kickable_type: String = "boulder" +var state: State = State.IDLE +var fly_vel: Vector3 = Vector3.ZERO +var health: float = 150.0 +var dead: bool = false +var damage_modifier: float = 1.8 + +@onready var mesh_node: MeshInstance3D = $BoulderMesh +var boulder_mat: StandardMaterial3D + +const COLOR_IDLE := Color(0.32, 0.28, 0.22) +const COLOR_IMPACT := Color(1.0, 1.0, 1.0) + +func _ready() -> void: + add_to_group("kickable") + boulder_mat = mesh_node.material_override.duplicate() as StandardMaterial3D + mesh_node.material_override = boulder_mat + +func apply_collision_damage(dmg: float) -> void: + _take_damage(dmg) + +func receive_kick(direction: Vector3, force: float) -> void: + fly_vel = direction * (force * 0.6) + fly_vel.y = 0.0 + state = State.FLYING + +func _physics_process(delta: float) -> void: + if state == State.IDLE: + return + _fly(delta) + +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 handled := 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 normal := col.get_normal() + normal.y = 0.0 + if normal.length() > 0.01: + fly_vel = fly_vel.bounce(normal.normalized()) * WALL_BOUNCE + else: + fly_vel = Vector3.ZERO + _take_damage(speed_now * WALL_SELF_DMG) + handled = true + break + elif col3d.is_in_group("enemies") or col3d.is_in_group("kickable"): + if col3d == self: + continue + KickSystem.resolve(self, col3d, fly_vel) + if not dead and is_instance_valid(col3d): + var kick_dir := col3d.global_position - global_position + kick_dir.y = 0.0 + if kick_dir.length() > 0.01: + col3d.call("receive_kick", kick_dir.normalized(), speed_now * 0.5) + fly_vel *= 0.55 + handled = true + break + + if not handled: + 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() < MIN_SPEED: + fly_vel = Vector3.ZERO + velocity = Vector3.ZERO + state = State.IDLE + + mesh_node.rotation.y += delta * speed_now * 0.15 + +func _take_damage(dmg: float) -> void: + if dead: + return + health -= dmg + _flash() + if health <= 0.0: + _die() + +func _die() -> void: + dead = true + state = State.IDLE + set_physics_process(false) + emit_signal("destroyed") + var tw := create_tween() + tw.tween_property(self, "scale", Vector3(2.0, 0.1, 2.0), 0.15) + tw.tween_property(self, "scale", Vector3(0.0, 0.0, 0.0), 0.12) + tw.tween_callback(queue_free) + +func _flash() -> void: + var tw := create_tween() + tw.tween_property(boulder_mat, "albedo_color", COLOR_IMPACT, 0.04) + var target_color := COLOR_IDLE.lerp(Color.RED, clampf(1.0 - health / 150.0, 0.0, 0.6)) + tw.tween_property(boulder_mat, "albedo_color", target_color, 0.18) diff --git a/scripts/Boulder.gd.uid b/scripts/Boulder.gd.uid new file mode 100644 index 0000000..c2e094c --- /dev/null +++ b/scripts/Boulder.gd.uid @@ -0,0 +1 @@ +uid://c1fcxciue3squ diff --git a/scripts/Enemy.gd b/scripts/Enemy.gd index a6143b9..d5414ff 100644 --- a/scripts/Enemy.gd +++ b/scripts/Enemy.gd @@ -1,12 +1,17 @@ +class_name Enemy extends CharacterBody3D -const PICKUP_SCENE := preload("res://scenes/Pickup.tscn") +const PICKUP_SCENE := preload("res://scenes/Pickup.tscn") +const LEATHER_SCENE := preload("res://scenes/Leather.tscn") signal died(points: int) signal merged(upgrade: bool) enum State { CHASING, FLYING, STUNNED, DEAD, MERGING } +static var first_leather_spawned: bool = false + +var kickable_type: String = "" var move_speed: float = 3.0 var health: int = 30 var damage_to_player: int = 8 @@ -46,6 +51,7 @@ func _ready() -> void: func setup(type: String, wave: int) -> void: enemy_type = type + kickable_type = type wave_num = wave match type: "slime": @@ -107,7 +113,7 @@ const AVOID_STRENGTH := 2.2 func _avoid_rocks(desired: Vector3) -> Vector3: var push := Vector3.ZERO - for rock in get_tree().get_nodes_in_group("rocks"): + for rock in get_tree().get_nodes_in_group("kickable"): if not is_instance_valid(rock): continue var away := global_position - (rock as Node3D).global_position @@ -147,7 +153,7 @@ func _fly(delta: float) -> void: 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("rocks"): + elif col3d.is_in_group("kickable"): KickSystem.resolve(self, col3d, fly_vel) var rock_dir := col3d.global_position - global_position rock_dir.y = 0.0 @@ -251,6 +257,14 @@ func _wall_impact_effect() -> void: tw.tween_property(mat, "albedo_color", COLOR_STUN, 0.12) func _try_drop_pickup() -> void: + if enemy_level == 1: + var drop_leather := not first_leather_spawned or randf() < 0.20 + if drop_leather: + first_leather_spawned = true + var leather := LEATHER_SCENE.instantiate() as Node3D + get_parent().add_child(leather) + leather.global_position = global_position + var roll := randf() var p_type := "" var p_heal := 0 diff --git a/scripts/KickSystem.gd b/scripts/KickSystem.gd index d22567b..d082563 100644 --- a/scripts/KickSystem.gd +++ b/scripts/KickSystem.gd @@ -24,12 +24,21 @@ static func resolve(owner: Node3D, other: Node3D, owner_vel: Vector3) -> bool: var speed_b := Vector2(other_vel.x, other_vel.z).length() var collision_speed := speed_a + speed_b - # ── 1. Merge ────────────────────────────────────────────────────────────── + # ── 1. Recipe merge (MergeRecipes) ──────────────────────────────────────── + var kt_a: String = owner.get("kickable_type") if owner.get("kickable_type") != null else "" + var kt_b: String = other.get("kickable_type") if other.get("kickable_type") != null else "" + if kt_a != "" and kt_b != "": + var recipe: Dictionary = MergeRecipes.find(kt_a, kt_b, collision_speed) + if not recipe.is_empty(): + _execute_recipe(owner, other, recipe) + return true + + # ── 2. Merge (enemy-to-enemy) ───────────────────────────────────────────── if owner.has_method("can_merge_with") and owner.call("can_merge_with", other, collision_speed): owner.call("do_merge_with", other) return true - # ── 2. Damage ───────────────────────────────────────────────────────────── + # ── 3. Damage ───────────────────────────────────────────────────────────── var mod_a: float = owner.get("damage_modifier") if owner.get("damage_modifier") != null else 0.0 var mod_b: float = other.get("damage_modifier") if other.get("damage_modifier") != null else 0.0 @@ -39,3 +48,15 @@ static func resolve(owner: Node3D, other: Node3D, owner_vel: Vector3) -> bool: owner.call("apply_collision_damage", collision_speed * mod_b) return false + +static func _execute_recipe(a: Node3D, b: Node3D, recipe: Dictionary) -> void: + var pos := (a.global_position + b.global_position) * 0.5 + var parent := a.get_parent() + a.queue_free() + b.queue_free() + var scene: PackedScene = load(recipe["result_scene"]) + if scene == null or parent == null: + return + var result := scene.instantiate() as Node3D + parent.add_child(result) + result.global_position = pos diff --git a/scripts/Leather.gd b/scripts/Leather.gd new file mode 100644 index 0000000..c09c63b --- /dev/null +++ b/scripts/Leather.gd @@ -0,0 +1,77 @@ +extends CharacterBody3D + +enum State { IDLE, FLYING } + +const AIR_FRICTION := 0.90 +const MIN_SPEED := 0.3 +const WALL_BOUNCE := 0.7 + +var kickable_type: String = "leather" +var state: State = State.IDLE +var fly_vel: Vector3 = Vector3.ZERO +var damage_modifier: float = 0.0 + +@onready var mesh_node: MeshInstance3D = $LeatherMesh + +func _ready() -> void: + add_to_group("kickable") + +func apply_collision_damage(_dmg: float) -> void: + pass + +func receive_kick(direction: Vector3, force: float) -> void: + fly_vel = direction * force + fly_vel.y = 0.0 + state = State.FLYING + +func _physics_process(delta: float) -> void: + if state == State.IDLE: + return + _fly(delta) + +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 handled := 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 normal := col.get_normal() + normal.y = 0.0 + if normal.length() > 0.01: + fly_vel = fly_vel.bounce(normal.normalized()) * WALL_BOUNCE + else: + fly_vel = Vector3.ZERO + handled = true + break + elif col3d.is_in_group("enemies") or col3d.is_in_group("kickable"): + if col3d == self: + continue + KickSystem.resolve(self, col3d, fly_vel) + if is_instance_valid(col3d): + var kick_dir := col3d.global_position - global_position + kick_dir.y = 0.0 + if kick_dir.length() > 0.01: + col3d.call("receive_kick", kick_dir.normalized(), speed_now * 0.4) + fly_vel *= 0.35 + handled = true + break + + if not handled: + 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() < MIN_SPEED: + fly_vel = Vector3.ZERO + velocity = Vector3.ZERO + state = State.IDLE + + mesh_node.rotation.y += delta * speed_now * 0.2 diff --git a/scripts/Leather.gd.uid b/scripts/Leather.gd.uid new file mode 100644 index 0000000..e6323a7 --- /dev/null +++ b/scripts/Leather.gd.uid @@ -0,0 +1 @@ +uid://dndcs6xc7m08x diff --git a/scripts/LeatherBoots.gd b/scripts/LeatherBoots.gd new file mode 100644 index 0000000..fe27822 --- /dev/null +++ b/scripts/LeatherBoots.gd @@ -0,0 +1,25 @@ +extends Node3D + +var tier: int = 1 + +@onready var tooltip: Label3D = $Tooltip + +func _ready() -> void: + add_to_group("interactable") + tooltip.visible = false + var tw := create_tween().set_loops() + tw.tween_property(self, "position:y", 0.3, 0.7) + tw.tween_property(self, "position:y", 0.1, 0.7) + +func _process(delta: float) -> void: + rotation.y += delta * 1.2 + var players := get_tree().get_nodes_in_group("player") + if players.is_empty(): + tooltip.visible = false + return + var p := players[0] as Node3D + tooltip.visible = p != null and global_position.distance_to(p.global_position) < 2.5 + +func interact(player: Node) -> void: + player.call("apply_upgrade_boots", 10.0, tier) + queue_free() diff --git a/scripts/LeatherBoots.gd.uid b/scripts/LeatherBoots.gd.uid new file mode 100644 index 0000000..9aff57f --- /dev/null +++ b/scripts/LeatherBoots.gd.uid @@ -0,0 +1 @@ +uid://d0j8nw7eynmk8 diff --git a/scripts/Main.gd b/scripts/Main.gd index 6d3990a..a9d3d8a 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -3,6 +3,7 @@ extends Node3D const PLAYER_SCENE := preload("res://scenes/Player.tscn") const ENEMY_SCENE := preload("res://scenes/Enemy.tscn") const ROCK_SCENE := preload("res://scenes/Rock.tscn") +const STICK_SCENE := preload("res://scenes/Stick.tscn") const LEVEL_SCENE := preload("res://scenes/Level.tscn") const CAM_DIST := 8.0 @@ -43,6 +44,7 @@ func _ready() -> void: _create_ui() _spawn_player() _spawn_rocks() + _spawn_sticks() _start_game() add_to_group("main") Input.mouse_mode = Input.MOUSE_MODE_CAPTURED @@ -104,14 +106,20 @@ func _spawn_rocks() -> void: for i in range(limit): _spawn_single_rock() +func _spawn_sticks() -> void: + for i in range(2): + var stick := STICK_SCENE.instantiate() + stick.position = _safe_item_position() + add_child(stick) + func _spawn_single_rock() -> void: var rock := ROCK_SCENE.instantiate() - rock.position = _safe_rock_position() + rock.position = _safe_item_position() add_child(rock) rock.connect("destroyed", _on_rock_destroyed) rocks_on_field += 1 -func _safe_rock_position() -> Vector3: +func _safe_item_position() -> Vector3: var player_pos := player.global_position if is_instance_valid(player) else Vector3.ZERO for _attempt in range(30): var angle := randf() * TAU @@ -120,7 +128,7 @@ func _safe_rock_position() -> Vector3: if player_pos.distance_to(pos) < 4.5: continue var clear := true - for r in get_tree().get_nodes_in_group("rocks"): + for r in get_tree().get_nodes_in_group("kickable"): if (r as Node3D).global_position.distance_to(pos) < 1.5: clear = false break @@ -162,6 +170,7 @@ func _start_game() -> void: score = 0 kills = 0 kills_for_next = 10 + Enemy.first_leather_spawned = false _update_labels() spawn_timer.wait_time = 1.4 spawn_timer.connect("timeout", _on_spawn_timer) diff --git a/scripts/MergeRecipes.gd b/scripts/MergeRecipes.gd new file mode 100644 index 0000000..4918abc --- /dev/null +++ b/scripts/MergeRecipes.gd @@ -0,0 +1,25 @@ +class_name MergeRecipes + +# Add new crafting recipes here. +# speed_threshold = minimum collision_speed to trigger the merge. +static var _list: Array[Dictionary] = [ + { + "ingredients": ["leather", "stick"], + "result_scene": "res://scenes/LeatherBoots.tscn", + "speed_threshold": 0.5, + }, + { + "ingredients": ["rock", "rock"], + "result_scene": "res://scenes/Boulder.tscn", + "speed_threshold": 5.0, + }, +] + +static func find(type_a: String, type_b: String, speed: float) -> Dictionary: + for r in _list: + var a: String = r["ingredients"][0] + var b: String = r["ingredients"][1] + var match_ab := (a == type_a and b == type_b) or (a == type_b and b == type_a) + if match_ab and speed >= float(r.get("speed_threshold", 3.0)): + return r + return {} diff --git a/scripts/MergeRecipes.gd.uid b/scripts/MergeRecipes.gd.uid new file mode 100644 index 0000000..d64a0d5 --- /dev/null +++ b/scripts/MergeRecipes.gd.uid @@ -0,0 +1 @@ +uid://uy8s3uoktnly diff --git a/scripts/Player.gd b/scripts/Player.gd index 2bd1e3c..adc779e 100644 --- a/scripts/Player.gd +++ b/scripts/Player.gd @@ -67,6 +67,11 @@ 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 @@ -130,7 +135,7 @@ func _do_kick() -> void: forward.y = 0.0 forward = forward.normalized() if forward.length() > 0.01 else Vector3(0.0, 0.0, -1.0) var half_cos: float = cos(deg_to_rad(kick_angle * 0.5)) - var enemies := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("rocks") + var enemies := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("kickable") var kicked_any := false for e in enemies: if not is_instance_valid(e): @@ -149,6 +154,20 @@ func _do_kick() -> void: if kicked_any: _squish_effect() +func _try_interact() -> void: + var best: Node3D = null + var best_dist := 2.5 + for node in get_tree().get_nodes_in_group("interactable"): + var n := node as Node3D + if n == null: + continue + var d := global_position.distance_to(n.global_position) + if d < best_dist: + best_dist = d + best = n + if best != null: + best.call("interact", self) + func set_aim_direction(yaw_rad: float) -> void: _aim_yaw = yaw_rad _is_aiming = true @@ -197,3 +216,9 @@ func apply_upgrade(id: String) -> void: max_health += 30 health = min(health + 30, max_health) emit_signal("health_changed", health, max_health) + +func apply_upgrade_boots(speed_bonus: float, _tier: int) -> void: + move_speed += speed_bonus + var tw := create_tween() + tw.tween_property(player_mat, "albedo_color", Color(1.0, 0.85, 0.2), 0.1) + tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.4) diff --git a/scripts/Rock.gd b/scripts/Rock.gd index 3cbe21b..7c7680c 100644 --- a/scripts/Rock.gd +++ b/scripts/Rock.gd @@ -9,6 +9,7 @@ const MIN_SPEED := 0.5 const WALL_BOUNCE := 0.5 const WALL_SELF_DMG := 0.6 +var kickable_type: String = "rock" var state: State = State.IDLE var fly_vel: Vector3 = Vector3.ZERO var health: float = 60.0 @@ -22,7 +23,7 @@ const COLOR_IDLE := Color(0.45, 0.38, 0.30) const COLOR_IMPACT := Color(1.0, 1.0, 1.0) func _ready() -> void: - add_to_group("rocks") + add_to_group("kickable") rock_mat = mesh_node.material_override.duplicate() as StandardMaterial3D mesh_node.material_override = rock_mat @@ -64,7 +65,7 @@ func _fly(delta: float) -> void: _take_damage(speed_now * WALL_SELF_DMG) handled = true break - elif col3d.is_in_group("enemies") or col3d.is_in_group("rocks"): + elif col3d.is_in_group("enemies") or col3d.is_in_group("kickable"): if col3d == self: continue KickSystem.resolve(self, col3d, fly_vel) diff --git a/scripts/Stick.gd b/scripts/Stick.gd new file mode 100644 index 0000000..e96ae10 --- /dev/null +++ b/scripts/Stick.gd @@ -0,0 +1,113 @@ +extends CharacterBody3D + +signal destroyed + +enum State { IDLE, FLYING } + +const AIR_FRICTION := 0.84 +const MIN_SPEED := 0.5 +const WALL_BOUNCE := 0.5 +const WALL_SELF_DMG := 0.5 + +var kickable_type: String = "stick" +var state: State = State.IDLE +var fly_vel: Vector3 = Vector3.ZERO +var health: float = 40.0 +var dead: bool = false +var damage_modifier: float = 0.6 + +@onready var mesh_node: MeshInstance3D = $StickMesh +var stick_mat: StandardMaterial3D + +const COLOR_IDLE := Color(0.55, 0.38, 0.18) +const COLOR_IMPACT := Color(1.0, 1.0, 1.0) + +func _ready() -> void: + add_to_group("kickable") + stick_mat = mesh_node.material_override.duplicate() as StandardMaterial3D + mesh_node.material_override = stick_mat + +func apply_collision_damage(dmg: float) -> void: + _take_damage(dmg) + +func receive_kick(direction: Vector3, force: float) -> void: + fly_vel = direction * force + fly_vel.y = 0.0 + state = State.FLYING + +func _physics_process(delta: float) -> void: + if state == State.IDLE: + return + _fly(delta) + +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 handled := 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 normal := col.get_normal() + normal.y = 0.0 + if normal.length() > 0.01: + fly_vel = fly_vel.bounce(normal.normalized()) * WALL_BOUNCE + else: + fly_vel = Vector3.ZERO + _take_damage(speed_now * WALL_SELF_DMG) + handled = true + break + elif col3d.is_in_group("enemies") or col3d.is_in_group("kickable"): + if col3d == self: + continue + KickSystem.resolve(self, col3d, fly_vel) + if not dead and is_instance_valid(col3d): + var kick_dir := col3d.global_position - global_position + kick_dir.y = 0.0 + if kick_dir.length() > 0.01: + col3d.call("receive_kick", kick_dir.normalized(), speed_now * 0.65) + fly_vel *= 0.45 + handled = true + break + + if not handled: + 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() < MIN_SPEED: + fly_vel = Vector3.ZERO + velocity = Vector3.ZERO + state = State.IDLE + + mesh_node.rotation.z += delta * speed_now * 0.3 + +func _take_damage(dmg: float) -> void: + if dead: + return + health -= dmg + _flash() + if health <= 0.0: + _die() + +func _die() -> void: + dead = true + state = State.IDLE + set_physics_process(false) + emit_signal("destroyed") + var tw := create_tween() + tw.tween_property(self, "scale", Vector3(1.6, 0.1, 1.6), 0.12) + tw.tween_property(self, "scale", Vector3(0.0, 0.0, 0.0), 0.1) + tw.tween_callback(queue_free) + +func _flash() -> void: + var tw := create_tween() + tw.tween_property(stick_mat, "albedo_color", COLOR_IMPACT, 0.04) + var target_color := COLOR_IDLE.lerp(Color.RED, clampf(1.0 - health / 40.0, 0.0, 0.6)) + tw.tween_property(stick_mat, "albedo_color", target_color, 0.18) diff --git a/scripts/Stick.gd.uid b/scripts/Stick.gd.uid new file mode 100644 index 0000000..7d50417 --- /dev/null +++ b/scripts/Stick.gd.uid @@ -0,0 +1 @@ +uid://ceo530cbtsr6e