WIP: merge recipies

This commit is contained in:
2026-04-22 19:10:44 +03:00
parent 66b8120eb7
commit 663d532c88
19 changed files with 546 additions and 11 deletions
+27
View File
@@ -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")
+25
View File
@@ -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")
+27
View File
@@ -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
+28
View File
@@ -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")
+113
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://c1fcxciue3squ
+17 -3
View File
@@ -1,12 +1,17 @@
class_name Enemy
extends CharacterBody3D 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 died(points: int)
signal merged(upgrade: bool) signal merged(upgrade: bool)
enum State { CHASING, FLYING, STUNNED, DEAD, MERGING } 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 move_speed: float = 3.0
var health: int = 30 var health: int = 30
var damage_to_player: int = 8 var damage_to_player: int = 8
@@ -46,6 +51,7 @@ func _ready() -> void:
func setup(type: String, wave: int) -> void: func setup(type: String, wave: int) -> void:
enemy_type = type enemy_type = type
kickable_type = type
wave_num = wave wave_num = wave
match type: match type:
"slime": "slime":
@@ -107,7 +113,7 @@ const AVOID_STRENGTH := 2.2
func _avoid_rocks(desired: Vector3) -> Vector3: func _avoid_rocks(desired: Vector3) -> Vector3:
var push := Vector3.ZERO 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): if not is_instance_valid(rock):
continue continue
var away := global_position - (rock as Node3D).global_position var away := global_position - (rock as Node3D).global_position
@@ -147,7 +153,7 @@ func _fly(delta: float) -> void:
chain_dir.y = 0.0 chain_dir.y = 0.0
if chain_dir.length() > 0.01: if chain_dir.length() > 0.01:
col3d.call("receive_kick", chain_dir.normalized(), speed_now * chain_factor) 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) KickSystem.resolve(self, col3d, fly_vel)
var rock_dir := col3d.global_position - global_position var rock_dir := col3d.global_position - global_position
rock_dir.y = 0.0 rock_dir.y = 0.0
@@ -251,6 +257,14 @@ func _wall_impact_effect() -> void:
tw.tween_property(mat, "albedo_color", COLOR_STUN, 0.12) tw.tween_property(mat, "albedo_color", COLOR_STUN, 0.12)
func _try_drop_pickup() -> void: 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 roll := randf()
var p_type := "" var p_type := ""
var p_heal := 0 var p_heal := 0
+23 -2
View File
@@ -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 speed_b := Vector2(other_vel.x, other_vel.z).length()
var collision_speed := speed_a + speed_b 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): if owner.has_method("can_merge_with") and owner.call("can_merge_with", other, collision_speed):
owner.call("do_merge_with", other) owner.call("do_merge_with", other)
return true 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_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 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) owner.call("apply_collision_damage", collision_speed * mod_b)
return false 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
+77
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://dndcs6xc7m08x
+25
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://d0j8nw7eynmk8
+12 -3
View File
@@ -3,6 +3,7 @@ extends Node3D
const PLAYER_SCENE := preload("res://scenes/Player.tscn") const PLAYER_SCENE := preload("res://scenes/Player.tscn")
const ENEMY_SCENE := preload("res://scenes/Enemy.tscn") const ENEMY_SCENE := preload("res://scenes/Enemy.tscn")
const ROCK_SCENE := preload("res://scenes/Rock.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 LEVEL_SCENE := preload("res://scenes/Level.tscn")
const CAM_DIST := 8.0 const CAM_DIST := 8.0
@@ -43,6 +44,7 @@ func _ready() -> void:
_create_ui() _create_ui()
_spawn_player() _spawn_player()
_spawn_rocks() _spawn_rocks()
_spawn_sticks()
_start_game() _start_game()
add_to_group("main") add_to_group("main")
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
@@ -104,14 +106,20 @@ func _spawn_rocks() -> void:
for i in range(limit): for i in range(limit):
_spawn_single_rock() _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: func _spawn_single_rock() -> void:
var rock := ROCK_SCENE.instantiate() var rock := ROCK_SCENE.instantiate()
rock.position = _safe_rock_position() rock.position = _safe_item_position()
add_child(rock) add_child(rock)
rock.connect("destroyed", _on_rock_destroyed) rock.connect("destroyed", _on_rock_destroyed)
rocks_on_field += 1 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 var player_pos := player.global_position if is_instance_valid(player) else Vector3.ZERO
for _attempt in range(30): for _attempt in range(30):
var angle := randf() * TAU var angle := randf() * TAU
@@ -120,7 +128,7 @@ func _safe_rock_position() -> Vector3:
if player_pos.distance_to(pos) < 4.5: if player_pos.distance_to(pos) < 4.5:
continue continue
var clear := true 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: if (r as Node3D).global_position.distance_to(pos) < 1.5:
clear = false clear = false
break break
@@ -162,6 +170,7 @@ func _start_game() -> void:
score = 0 score = 0
kills = 0 kills = 0
kills_for_next = 10 kills_for_next = 10
Enemy.first_leather_spawned = false
_update_labels() _update_labels()
spawn_timer.wait_time = 1.4 spawn_timer.wait_time = 1.4
spawn_timer.connect("timeout", _on_spawn_timer) spawn_timer.connect("timeout", _on_spawn_timer)
+25
View File
@@ -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 {}
+1
View File
@@ -0,0 +1 @@
uid://uy8s3uoktnly
+26 -1
View File
@@ -67,6 +67,11 @@ func _make_kick_arc_mesh() -> ArrayMesh:
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh 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: func _physics_process(delta: float) -> void:
if not is_alive: if not is_alive:
return return
@@ -130,7 +135,7 @@ func _do_kick() -> void:
forward.y = 0.0 forward.y = 0.0
forward = forward.normalized() if forward.length() > 0.01 else Vector3(0.0, 0.0, -1.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 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 var kicked_any := false
for e in enemies: for e in enemies:
if not is_instance_valid(e): if not is_instance_valid(e):
@@ -149,6 +154,20 @@ func _do_kick() -> void:
if kicked_any: if kicked_any:
_squish_effect() _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: func set_aim_direction(yaw_rad: float) -> void:
_aim_yaw = yaw_rad _aim_yaw = yaw_rad
_is_aiming = true _is_aiming = true
@@ -197,3 +216,9 @@ func apply_upgrade(id: String) -> void:
max_health += 30 max_health += 30
health = min(health + 30, max_health) health = min(health + 30, max_health)
emit_signal("health_changed", health, 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)
+3 -2
View File
@@ -9,6 +9,7 @@ const MIN_SPEED := 0.5
const WALL_BOUNCE := 0.5 const WALL_BOUNCE := 0.5
const WALL_SELF_DMG := 0.6 const WALL_SELF_DMG := 0.6
var kickable_type: String = "rock"
var state: State = State.IDLE var state: State = State.IDLE
var fly_vel: Vector3 = Vector3.ZERO var fly_vel: Vector3 = Vector3.ZERO
var health: float = 60.0 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) const COLOR_IMPACT := Color(1.0, 1.0, 1.0)
func _ready() -> void: func _ready() -> void:
add_to_group("rocks") add_to_group("kickable")
rock_mat = mesh_node.material_override.duplicate() as StandardMaterial3D rock_mat = mesh_node.material_override.duplicate() as StandardMaterial3D
mesh_node.material_override = rock_mat mesh_node.material_override = rock_mat
@@ -64,7 +65,7 @@ func _fly(delta: float) -> void:
_take_damage(speed_now * WALL_SELF_DMG) _take_damage(speed_now * WALL_SELF_DMG)
handled = true handled = true
break 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: if col3d == self:
continue continue
KickSystem.resolve(self, col3d, fly_vel) KickSystem.resolve(self, col3d, fly_vel)
+113
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://ceo530cbtsr6e