boss phase

This commit is contained in:
2026-04-23 15:38:45 +03:00
parent 5d843f94f7
commit 3974d7416d
+243 -3
View File
@@ -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