From 3974d7416d7ef5b96db29ea653bb66c50afffbe4 Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov Date: Thu, 23 Apr 2026 15:38:45 +0300 Subject: [PATCH] boss phase --- scripts/Main.gd | 246 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 3 deletions(-) diff --git a/scripts/Main.gd b/scripts/Main.gd index 7fd2910..3a46cac 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -53,6 +53,15 @@ var equip_fills: Array[ColorRect] = [] var equip_labels: Array[Label] = [] var _equip_prev_tiers: Array[int] = [-1, -1, -1] +# Boss phase +var boss_active: bool = false +var first_boss_spawned: bool = false +var boss_timer: float = 90.0 +var portal_node: Node3D = null +var boss_timer_label: Label +var boss_hint_label: Label +var win_panel: Panel + func _ready() -> void: _spawn_level() _create_camera() @@ -112,6 +121,28 @@ func _process(delta: float) -> void: player.set_aim_direction(deg_to_rad(cam_yaw)) _update_tier_label() + if boss_active: + boss_timer = max(0.0, boss_timer - delta) + var mins := int(boss_timer) / 60 + var secs := int(boss_timer) % 60 + boss_timer_label.text = "%d:%02d" % [mins, secs] + if boss_timer <= 10.0: + boss_timer_label.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2)) + if boss_timer <= 0.0: + _trigger_time_up() + return + if is_instance_valid(portal_node): + var area := portal_node.get_node_or_null("PortalArea") as Area3D + if area != null: + for body in area.get_overlapping_bodies(): + if body.get("enemy_level") == 3: + var fv = body.get("fly_vel") + if fv != null: + var spd := Vector2((fv as Vector3).x, (fv as Vector3).z).length() + if spd >= 25.0: + _trigger_win() + return + # ─── Rocks ──────────────────────────────────────────────────────────────────── var rocks_on_field: int = 0 @@ -236,6 +267,9 @@ func _spawn_enemy() -> void: enemy.target = player enemy.connect("died", _on_enemy_died) enemy.connect("merged", _on_enemy_merged) + if type == "ogre" and not first_boss_spawned: + first_boss_spawned = true + _start_boss_phase() # Spawn at random edge var side := randi() % 4 @@ -286,9 +320,9 @@ func _spawn_upgraded_enemy(pos: Vector3, type: String, level: int, w: int) -> Ch (col_shape.shape as BoxShape3D).size = old_size * s var color := Color(1.0, 1.0, 0.5) if level > 2 else Color(1.0, 0.9, 0.3) tw.tween_property(enemy.mat, "albedo_color", color, 0.25) - #var bs := scale - #tw.tween_property(enemy.mesh_node, "scale", Vector3(bs * 1.6, bs * 0.25, bs * 1.6), 0.07) - #tw.tween_property(enemy.mesh_node, "scale", Vector3(bs, bs, bs), 0.22) + if level >= 3 and not first_boss_spawned: + first_boss_spawned = true + _start_boss_phase() return enemy func _on_enemy_merged(_upgrade: bool) -> void: @@ -296,6 +330,12 @@ func _on_enemy_merged(_upgrade: bool) -> void: func _on_player_died() -> void: game_active = false + if boss_active: + boss_active = false + boss_timer_label.visible = false + boss_hint_label.visible = false + if is_instance_valid(portal_node): + portal_node.queue_free() spawn_timer.stop() _show_gameover() @@ -352,6 +392,125 @@ func _restart() -> void: get_tree().paused = false get_tree().reload_current_scene() +# ─── Boss phase ─────────────────────────────────────────────────────────────── + +func _start_boss_phase() -> void: + if boss_active or not game_active: + return + boss_active = true + boss_timer = 90.0 + boss_timer_label.visible = true + boss_hint_label.visible = true + _spawn_portal() + +func _spawn_portal() -> void: + portal_node = Node3D.new() + add_child(portal_node) + + var player_pos := player.global_position if is_instance_valid(player) else Vector3.ZERO + var best_pos := Vector3(arena_size - 1.5, 0.0, 0.0) + var best_dist := 0.0 + for _i in range(16): + var angle := randf() * TAU + var cand := Vector3(cos(angle), 0.0, sin(angle)) * (arena_size - 1.5) + var d := cand.distance_to(player_pos) + if d > best_dist: + best_dist = d + best_pos = cand + portal_node.global_position = best_pos + + var light := OmniLight3D.new() + light.light_color = Color(0.5, 0.1, 1.0) + light.omni_range = 7.0 + light.light_energy = 3.0 + light.position.y = 1.0 + portal_node.add_child(light) + + var ring := MeshInstance3D.new() + var torus := TorusMesh.new() + torus.inner_radius = 1.0 + torus.outer_radius = 1.65 + var ring_mat := StandardMaterial3D.new() + ring_mat.albedo_color = Color(0.55, 0.1, 1.0) + ring_mat.emission_enabled = true + ring_mat.emission = Color(0.6, 0.2, 1.0) + ring_mat.emission_energy = 3.5 + ring_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + torus.material = ring_mat + ring.mesh = torus + ring.position.y = 0.05 + portal_node.add_child(ring) + ring.create_tween().set_loops().tween_property(ring, "rotation:y", TAU, 2.5) + + var disc := MeshInstance3D.new() + var disc_mesh := CylinderMesh.new() + disc_mesh.top_radius = 1.0 + disc_mesh.bottom_radius = 1.0 + disc_mesh.height = 0.05 + var disc_mat := StandardMaterial3D.new() + disc_mat.albedo_color = Color(0.25, 0.0, 0.7, 0.65) + disc_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + disc_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + disc_mat.emission_enabled = true + disc_mat.emission = Color(0.4, 0.1, 1.0) + disc_mat.emission_energy = 1.5 + disc_mesh.material = disc_mat + disc.mesh = disc_mesh + disc.position.y = 0.03 + portal_node.add_child(disc) + var tw_pulse := portal_node.create_tween().set_loops() + tw_pulse.tween_method(func(e: float): disc_mat.emission_energy = e, 0.8, 3.0, 0.7) + tw_pulse.tween_method(func(e: float): disc_mat.emission_energy = e, 3.0, 0.8, 0.7) + + var area := Area3D.new() + area.name = "PortalArea" + var col := CollisionShape3D.new() + var cyl := CylinderShape3D.new() + cyl.radius = 1.8 + cyl.height = 3.0 + col.shape = cyl + col.position.y = 1.5 + area.add_child(col) + portal_node.add_child(area) + + var lbl := Label3D.new() + lbl.text = "PORTAL\nPush Ogre Here" + lbl.position = Vector3(0, 2.6, 0) + lbl.billboard = BaseMaterial3D.BILLBOARD_ENABLED + lbl.font_size = 34 + lbl.outline_size = 6 + lbl.modulate = Color(0.85, 0.55, 1.0) + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + portal_node.add_child(lbl) + + FX.merge_smoke(best_pos + Vector3(0, 0.5, 0), self) + +func _trigger_win() -> void: + if not boss_active: + return + boss_active = false + game_active = false + spawn_timer.stop() + boss_timer_label.visible = false + boss_hint_label.visible = false + if is_instance_valid(portal_node): + portal_node.queue_free() + get_tree().paused = true + win_panel.visible = true + (win_panel.get_node("VBox/ScoreLabel") as Label).text = "Score: %d\nWave: %d" % [score, wave] + +func _trigger_time_up() -> void: + if not boss_active: + return + boss_active = false + game_active = false + spawn_timer.stop() + boss_timer_label.visible = false + boss_hint_label.visible = false + if is_instance_valid(portal_node): + portal_node.queue_free() + _show_gameover() + # ─── Tutorial ───────────────────────────────────────────────────────────────── func _create_tutorial_overlay() -> void: @@ -452,6 +611,8 @@ func _create_ui() -> void: _make_hud() _make_upgrade_panel() _make_gameover_panel() + _make_boss_ui() + _make_win_panel() func _make_hud() -> void: # Score @@ -553,6 +714,85 @@ func _make_gameover_panel() -> void: restart_btn.connect("pressed", _restart) vbox.add_child(restart_btn) +func _make_boss_ui() -> void: + boss_timer_label = Label.new() + boss_timer_label.anchor_left = 0.5 + boss_timer_label.anchor_right = 0.5 + boss_timer_label.anchor_top = 0.0 + boss_timer_label.offset_left = -80 + boss_timer_label.offset_right = 80 + boss_timer_label.offset_top = 8 + boss_timer_label.offset_bottom = 54 + boss_timer_label.text = "1:30" + boss_timer_label.add_theme_font_size_override("font_size", 38) + boss_timer_label.add_theme_color_override("font_color", Color(0.8, 0.3, 1.0)) + boss_timer_label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.9)) + boss_timer_label.add_theme_constant_override("shadow_offset_x", 2) + boss_timer_label.add_theme_constant_override("shadow_offset_y", 2) + boss_timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + boss_timer_label.visible = false + canvas.add_child(boss_timer_label) + + boss_hint_label = Label.new() + boss_hint_label.anchor_left = 0.5 + boss_hint_label.anchor_right = 0.5 + boss_hint_label.anchor_top = 0.0 + boss_hint_label.offset_left = -220 + boss_hint_label.offset_right = 220 + boss_hint_label.offset_top = 54 + boss_hint_label.offset_bottom = 80 + boss_hint_label.text = "Push the Ogre into the Portal!" + boss_hint_label.add_theme_font_size_override("font_size", 17) + boss_hint_label.add_theme_color_override("font_color", Color(0.85, 0.7, 1.0)) + boss_hint_label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8)) + boss_hint_label.add_theme_constant_override("shadow_offset_x", 1) + boss_hint_label.add_theme_constant_override("shadow_offset_y", 1) + boss_hint_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + boss_hint_label.visible = false + canvas.add_child(boss_hint_label) + +func _make_win_panel() -> void: + win_panel = Panel.new() + win_panel.process_mode = Node.PROCESS_MODE_ALWAYS + win_panel.visible = false + canvas.add_child(win_panel) + + win_panel.anchor_left = 0.5 + win_panel.anchor_right = 0.5 + win_panel.anchor_top = 0.5 + win_panel.anchor_bottom = 0.5 + win_panel.offset_left = -200.0 + win_panel.offset_right = 200.0 + win_panel.offset_top = -140.0 + win_panel.offset_bottom = 140.0 + + var vbox2 := VBoxContainer.new() + vbox2.name = "VBox" + vbox2.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + vbox2.alignment = BoxContainer.ALIGNMENT_CENTER + vbox2.add_theme_constant_override("separation", 16) + win_panel.add_child(vbox2) + + var title2 := Label.new() + title2.text = "VICTORY!" + title2.add_theme_font_size_override("font_size", 42) + title2.add_theme_color_override("font_color", Color(1.0, 0.85, 0.1)) + title2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + vbox2.add_child(title2) + + var score_lbl2 := Label.new() + score_lbl2.name = "ScoreLabel" + score_lbl2.add_theme_font_size_override("font_size", 20) + score_lbl2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + vbox2.add_child(score_lbl2) + + var btn2 := Button.new() + btn2.text = "Play Again" + btn2.add_theme_font_size_override("font_size", 18) + btn2.process_mode = Node.PROCESS_MODE_ALWAYS + btn2.connect("pressed", _restart) + vbox2.add_child(btn2) + # ─── Equipment slots ────────────────────────────────────────────────────────── const _EQUIP_SLOT_SIZE := 54