boss phase
This commit is contained in:
+243
-3
@@ -53,6 +53,15 @@ var equip_fills: Array[ColorRect] = []
|
|||||||
var equip_labels: Array[Label] = []
|
var equip_labels: Array[Label] = []
|
||||||
var _equip_prev_tiers: Array[int] = [-1, -1, -1]
|
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:
|
func _ready() -> void:
|
||||||
_spawn_level()
|
_spawn_level()
|
||||||
_create_camera()
|
_create_camera()
|
||||||
@@ -112,6 +121,28 @@ func _process(delta: float) -> void:
|
|||||||
player.set_aim_direction(deg_to_rad(cam_yaw))
|
player.set_aim_direction(deg_to_rad(cam_yaw))
|
||||||
_update_tier_label()
|
_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 ────────────────────────────────────────────────────────────────────
|
# ─── Rocks ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
var rocks_on_field: int = 0
|
var rocks_on_field: int = 0
|
||||||
@@ -236,6 +267,9 @@ func _spawn_enemy() -> void:
|
|||||||
enemy.target = player
|
enemy.target = player
|
||||||
enemy.connect("died", _on_enemy_died)
|
enemy.connect("died", _on_enemy_died)
|
||||||
enemy.connect("merged", _on_enemy_merged)
|
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
|
# Spawn at random edge
|
||||||
var side := randi() % 4
|
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
|
(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)
|
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)
|
tw.tween_property(enemy.mat, "albedo_color", color, 0.25)
|
||||||
#var bs := scale
|
if level >= 3 and not first_boss_spawned:
|
||||||
#tw.tween_property(enemy.mesh_node, "scale", Vector3(bs * 1.6, bs * 0.25, bs * 1.6), 0.07)
|
first_boss_spawned = true
|
||||||
#tw.tween_property(enemy.mesh_node, "scale", Vector3(bs, bs, bs), 0.22)
|
_start_boss_phase()
|
||||||
return enemy
|
return enemy
|
||||||
|
|
||||||
func _on_enemy_merged(_upgrade: bool) -> void:
|
func _on_enemy_merged(_upgrade: bool) -> void:
|
||||||
@@ -296,6 +330,12 @@ func _on_enemy_merged(_upgrade: bool) -> void:
|
|||||||
|
|
||||||
func _on_player_died() -> void:
|
func _on_player_died() -> void:
|
||||||
game_active = false
|
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()
|
spawn_timer.stop()
|
||||||
_show_gameover()
|
_show_gameover()
|
||||||
|
|
||||||
@@ -352,6 +392,125 @@ func _restart() -> void:
|
|||||||
get_tree().paused = false
|
get_tree().paused = false
|
||||||
get_tree().reload_current_scene()
|
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 ─────────────────────────────────────────────────────────────────
|
# ─── Tutorial ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _create_tutorial_overlay() -> void:
|
func _create_tutorial_overlay() -> void:
|
||||||
@@ -452,6 +611,8 @@ func _create_ui() -> void:
|
|||||||
_make_hud()
|
_make_hud()
|
||||||
_make_upgrade_panel()
|
_make_upgrade_panel()
|
||||||
_make_gameover_panel()
|
_make_gameover_panel()
|
||||||
|
_make_boss_ui()
|
||||||
|
_make_win_panel()
|
||||||
|
|
||||||
func _make_hud() -> void:
|
func _make_hud() -> void:
|
||||||
# Score
|
# Score
|
||||||
@@ -553,6 +714,85 @@ func _make_gameover_panel() -> void:
|
|||||||
restart_btn.connect("pressed", _restart)
|
restart_btn.connect("pressed", _restart)
|
||||||
vbox.add_child(restart_btn)
|
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 ──────────────────────────────────────────────────────────
|
# ─── Equipment slots ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const _EQUIP_SLOT_SIZE := 54
|
const _EQUIP_SLOT_SIZE := 54
|
||||||
|
|||||||
Reference in New Issue
Block a user