WIP: merge recipies
This commit is contained in:
@@ -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")
|
||||||
@@ -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")
|
||||||
@@ -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
|
||||||
@@ -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")
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1fcxciue3squ
|
||||||
+17
-3
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dndcs6xc7m08x
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d0j8nw7eynmk8
|
||||||
+12
-3
@@ -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)
|
||||||
|
|||||||
@@ -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 {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://uy8s3uoktnly
|
||||||
+26
-1
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ceo530cbtsr6e
|
||||||
Reference in New Issue
Block a user