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/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/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/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