diff --git a/scenes/Pickup.tscn b/scenes/Pickup.tscn new file mode 100644 index 0000000..38b0e2d --- /dev/null +++ b/scenes/Pickup.tscn @@ -0,0 +1,19 @@ +[gd_scene format=3 uid="uid://c8pickup3mn5x"] + +[ext_resource type="Script" path="res://scripts/Pickup.gd" id="1_pickup"] + +[sub_resource type="SphereMesh" id="SphereMesh_1"] +radius = 0.18 +height = 0.36 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(1, 1, 1, 1) +roughness = 0.4 +metallic = 0.2 + +[node name="Pickup" type="Node3D"] +script = ExtResource("1_pickup") + +[node name="PickupMesh" type="MeshInstance3D" parent="."] +mesh = SubResource("SphereMesh_1") +material_override = SubResource("StandardMaterial3D_1") diff --git a/scenes/Rock.tscn b/scenes/Rock.tscn new file mode 100644 index 0000000..b8059f2 --- /dev/null +++ b/scenes/Rock.tscn @@ -0,0 +1,27 @@ +[gd_scene format=3 uid="uid://b3rock7gyam2k"] + +[ext_resource type="Script" path="res://scripts/Rock.gd" id="1_rock"] + +[sub_resource type="SphereMesh" id="SphereMesh_1"] +radius = 0.25 +height = 0.5 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(0.45, 0.38, 0.3, 1) +roughness = 1.0 +metallic = 0.05 + +[sub_resource type="SphereShape3D" id="SphereShape3D_1"] +radius = 0.25 + +[node name="Rock" type="CharacterBody3D"] +script = ExtResource("1_rock") + +[node name="RockMesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, 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.25, 0) +shape = SubResource("SphereShape3D_1") diff --git a/scripts/Enemy.gd b/scripts/Enemy.gd index 0a7e0f2..de7119d 100644 --- a/scripts/Enemy.gd +++ b/scripts/Enemy.gd @@ -1,5 +1,7 @@ extends CharacterBody3D +const PICKUP_SCENE := preload("res://scenes/Pickup.tscn") + signal died(points: int) signal merged(upgrade: bool) @@ -211,12 +213,34 @@ func _wall_impact_effect() -> void: tw.tween_property(mat, "albedo_color", Color.WHITE, 0.04) tw.tween_property(mat, "albedo_color", COLOR_STUN, 0.12) +func _try_drop_pickup() -> void: + var roll := randf() + var p_type := "" + var p_heal := 0 + match enemy_type: + "slime": + if roll < 0.30: p_type = "health"; p_heal = 15 + elif roll < 0.40: p_type = "score" + "bat": + if roll < 0.20: p_type = "health"; p_heal = 12 + elif roll < 0.25: p_type = "score" + "ogre": + if roll < 0.55: p_type = "health"; p_heal = 35 + elif roll < 0.80: p_type = "score" + if p_type == "": + return + var pickup := PICKUP_SCENE.instantiate() + pickup.setup(p_type, p_heal) + get_parent().add_child(pickup) + pickup.global_position = global_position + func _die() -> void: if state == State.DEAD: return state = State.DEAD set_physics_process(false) emit_signal("died", score_value) + _try_drop_pickup() 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) diff --git a/scripts/Main.gd b/scripts/Main.gd index 0c0d5cc..d7dc389 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -2,6 +2,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 ARENA := 14.0 const WALL_T := 1.2 @@ -42,6 +43,7 @@ func _ready() -> void: _create_camera() _create_ui() _spawn_player() + _spawn_rocks() _start_game() add_to_group("main") Input.mouse_mode = Input.MOUSE_MODE_CAPTURED @@ -187,6 +189,16 @@ func _process(delta: float) -> void: if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT): player.set_aim_direction(deg_to_rad(cam_yaw)) +# ─── Rocks ──────────────────────────────────────────────────────────────────── + +func _spawn_rocks() -> void: + for i in range(10): + var rock := ROCK_SCENE.instantiate() + add_child(rock) + var angle := randf() * TAU + var dist := randf_range(3.5, ARENA - 2.0) + rock.position = Vector3(cos(angle) * dist, 0.0, sin(angle) * dist) + # ─── Player ─────────────────────────────────────────────────────────────────── func _spawn_player() -> void: @@ -238,6 +250,10 @@ func _spawn_enemy() -> void: 2: enemy.position = Vector3(-(ARENA - 0.5), 0, r) 3: enemy.position = Vector3( (ARENA - 0.5), 0, r) +func add_bonus_score(amount: int) -> void: + score += amount + _update_labels() + func _on_enemy_died(points: int) -> void: score += points kills += 1 diff --git a/scripts/Pickup.gd b/scripts/Pickup.gd new file mode 100644 index 0000000..c2dbc13 --- /dev/null +++ b/scripts/Pickup.gd @@ -0,0 +1,62 @@ +extends Node3D + +var pickup_type: String = "health" +var heal_amount: int = 20 +var collected: bool = false +var bob_t: float = 0.0 + +@onready var mesh_node: MeshInstance3D = $PickupMesh +var mat: StandardMaterial3D + +const PICKUP_RADIUS := 1.0 +const COLORS := { + "health": Color(0.1, 0.9, 0.25), + "score": Color(1.0, 0.85, 0.1), +} + +func setup(p_type: String, p_heal: int = 20) -> void: + pickup_type = p_type + heal_amount = p_heal + +func _ready() -> void: + add_to_group("pickups") + bob_t = randf() * TAU + mat = mesh_node.material_override.duplicate() as StandardMaterial3D + mesh_node.material_override = mat + var col: Color = COLORS.get(pickup_type, Color.WHITE) + mat.albedo_color = col + mat.emission_enabled = true + mat.emission = col + mat.emission_energy_multiplier = 0.6 + scale = Vector3.ZERO + var tw := create_tween() + tw.tween_property(self, "scale", Vector3.ONE, 0.22).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) + +func _process(delta: float) -> void: + if collected: + return + bob_t += delta + mesh_node.position.y = 0.3 + sin(bob_t * 2.6) * 0.1 + mesh_node.rotation.y += delta * 2.2 + + for p in get_tree().get_nodes_in_group("player"): + if not is_instance_valid(p): + continue + if (p as Node3D).global_position.distance_to(global_position) < PICKUP_RADIUS: + _collect(p) + return + +func _collect(player: Node3D) -> void: + collected = true + match pickup_type: + "health": + player.call("heal", heal_amount) + "score": + for m in get_tree().get_nodes_in_group("main"): + if is_instance_valid(m): + m.call("add_bonus_score", 30) + break + var tw := create_tween() + tw.tween_property(self, "scale", Vector3(1.6, 1.6, 1.6), 0.07) + tw.tween_property(self, "scale", Vector3.ZERO, 0.1) + tw.tween_callback(queue_free) diff --git a/scripts/Pickup.gd.uid b/scripts/Pickup.gd.uid new file mode 100644 index 0000000..2057456 --- /dev/null +++ b/scripts/Pickup.gd.uid @@ -0,0 +1 @@ +uid://clakucffgo2ag diff --git a/scripts/Player.gd b/scripts/Player.gd index 5a06d92..2bd1e3c 100644 --- a/scripts/Player.gd +++ b/scripts/Player.gd @@ -130,7 +130,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") + var enemies := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("rocks") var kicked_any := false for e in enemies: if not is_instance_valid(e): @@ -170,6 +170,15 @@ func take_damage(amount: int) -> void: #if health <= 0: #_die() +func heal(amount: int) -> void: + if not is_alive: + return + health = min(health + amount, max_health) + emit_signal("health_changed", health, max_health) + var tw := create_tween() + tw.tween_property(player_mat, "albedo_color", Color(0.1, 1.0, 0.35), 0.08) + tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.3) + func _die() -> void: is_alive = false emit_signal("died") diff --git a/scripts/Rock.gd b/scripts/Rock.gd new file mode 100644 index 0000000..727beaa --- /dev/null +++ b/scripts/Rock.gd @@ -0,0 +1,105 @@ +extends CharacterBody3D + +enum State { IDLE, FLYING } + +const AIR_FRICTION := 0.84 +const MIN_SPEED := 0.5 +const DAMAGE_MULT := 0.9 +const WALL_BOUNCE := 0.5 +const WALL_SELF_DMG := 0.6 +const HIT_SELF_DMG := 0.4 + +var state: State = State.IDLE +var fly_vel: Vector3 = Vector3.ZERO +var health: float = 60.0 +var dead: bool = false + +@onready var mesh_node: MeshInstance3D = $RockMesh +var rock_mat: StandardMaterial3D + +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") + rock_mat = mesh_node.material_override.duplicate() as StandardMaterial3D + mesh_node.material_override = rock_mat + +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"): + var to_enemy := col3d.global_position - global_position + to_enemy.y = 0.0 + var dir := to_enemy.normalized() if to_enemy.length() > 0.01 else (fly_vel.normalized() if fly_vel.length() > 0.01 else Vector3.FORWARD) + col3d.call("_take_hit", int(speed_now * DAMAGE_MULT)) + col3d.call("receive_kick", dir, speed_now * 0.65) + fly_vel *= 0.45 + _take_damage(speed_now * HIT_SELF_DMG) + 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.25 + +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) + 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(rock_mat, "albedo_color", COLOR_IMPACT, 0.04) + var target_color := COLOR_IDLE.lerp(Color.RED, clampf(1.0 - health / 60.0, 0.0, 0.6)) + tw.tween_property(rock_mat, "albedo_color", target_color, 0.18) diff --git a/scripts/Rock.gd.uid b/scripts/Rock.gd.uid new file mode 100644 index 0000000..6f41ee9 --- /dev/null +++ b/scripts/Rock.gd.uid @@ -0,0 +1 @@ +uid://bqobsqconcup1