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 STICK_SCENE := preload("res://scenes/Stick.tscn") const LEVEL_SCENE := preload("res://scenes/Level.tscn") const CAM_DIST := 8.0 const MOUSE_SENS := 0.18 const PITCH_MIN := 5.0 const PITCH_MAX := 70.0 const SPAWN_TIME := 5 var arena_size: float = 14.0 var cam_yaw: float = 0.0 var cam_pitch: float = 28.0 @onready var spawn_timer: Timer = $SpawnTimer var player: CharacterBody3D var camera: Camera3D var wave: int = 1 var score: int = 0 var kills: int = 0 var kills_for_next: int = 10 var game_active: bool = false var upgrading: bool = false # Tutorial var tutorial_canvas: CanvasLayer var tutorial_image: TextureRect var tutorial_hint: Label var tutorial_hint_ready: bool = false var tutorial_active: bool = false var tutorial_on_dismiss: Callable = Callable() var shown_tutorials: Dictionary = {} # UI nodes var canvas: CanvasLayer var score_label: Label var wave_label: Label var tier_label: Label var hp_bar: ColorRect var hp_bar_bg: ColorRect var progress_bar: ColorRect var progress_bg: ColorRect var upgrade_panel: Panel var gameover_panel: Panel var pause_panel: Panel var pause_image: TextureRect var pause_toggle_btn: Button var _pause_showing_craft: bool = true # Equipment slots 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 first_bat_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() _create_ui() _create_tutorial_overlay() _spawn_player() _spawn_rocks() _spawn_sticks() add_to_group("main") Input.mouse_mode = Input.MOUSE_MODE_CAPTURED show_tutorial("Tutorial_StartGame", _start_game) func _input(event: InputEvent) -> void: if tutorial_active: return var motion := event as InputEventMouseMotion if motion != null and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: cam_yaw -= motion.relative.x * MOUSE_SENS cam_pitch += motion.relative.y * MOUSE_SENS cam_pitch = clampf(cam_pitch, PITCH_MIN, PITCH_MAX) if event.is_action_pressed("ui_cancel"): if game_active and not upgrading: _toggle_pause_menu() else: if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE else: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if OS.is_debug_build(): var key := event as InputEventKey if key != null and key.pressed and not key.echo: if key.keycode == KEY_F5: boss_active = true _trigger_win() elif key.keycode == KEY_F6: _on_player_died() # ─── Level ──────────────────────────────────────────────────────────────────── func _spawn_level() -> void: var level := LEVEL_SCENE.instantiate() add_child(level) var sz = level.get("arena_size") if sz != null: arena_size = float(sz) # ─── Camera ─────────────────────────────────────────────────────────────────── func _create_camera() -> void: camera = Camera3D.new() camera.fov = 70.0 add_child(camera) func _process(delta: float) -> void: if is_instance_valid(player): var yaw_r: float = deg_to_rad(cam_yaw) var pitch_r: float = deg_to_rad(cam_pitch) var offset := Vector3( sin(yaw_r) * cos(pitch_r) * CAM_DIST, sin(pitch_r) * CAM_DIST, cos(yaw_r) * cos(pitch_r) * CAM_DIST ) var look_at_pos := player.global_position + Vector3(0, 0.8, 0) camera.global_position = camera.global_position.lerp(look_at_pos + offset, 14.0 * delta) camera.look_at(look_at_pos, Vector3.UP) 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 var rocks_pending: int = 0 func _get_rock_limit() -> int: return mini(3 + (wave + 1) / 2, 7) func _spawn_rocks() -> void: var limit := _get_rock_limit() for i in range(limit): _spawn_single_rock() var sticks_on_field: int = 0 var sticks_pending: int = 0 const STICK_LIMIT := 3 func _spawn_sticks() -> void: for i in range(STICK_LIMIT): _spawn_single_stick() func _spawn_single_stick() -> void: var stick := STICK_SCENE.instantiate() stick.position = _safe_item_position() add_child(stick) stick.connect("destroyed", _on_stick_destroyed) sticks_on_field += 1 func _on_stick_destroyed() -> void: sticks_on_field = maxi(0, sticks_on_field - 1) if not game_active: return if sticks_on_field + sticks_pending < STICK_LIMIT: sticks_pending += 1 await get_tree().create_timer(GameSettings.item_respawn_delay).timeout sticks_pending -= 1 if game_active: _spawn_single_stick() func _spawn_single_rock() -> void: var rock := ROCK_SCENE.instantiate() rock.position = _safe_item_position() add_child(rock) rock.connect("destroyed", _on_rock_destroyed) rocks_on_field += 1 func _safe_item_position() -> Vector3: var player_pos := player.global_position if is_instance_valid(player) else Vector3.ZERO for _attempt in range(30): var angle := randf() * TAU var dist := randf_range(4.0, arena_size - 2.0) var pos := Vector3(cos(angle) * dist, 0.0, sin(angle) * dist) if player_pos.distance_to(pos) < 4.5: continue var clear := true for r in get_tree().get_nodes_in_group("kickable"): if (r as Node3D).global_position.distance_to(pos) < 1.5: clear = false break if clear: return pos var a := randf() * TAU return Vector3(cos(a) * (arena_size - 2.5), 0.0, sin(a) * (arena_size - 2.5)) func _on_rock_destroyed() -> void: rocks_on_field = maxi(0, rocks_on_field - 1) if not game_active: return if rocks_on_field + rocks_pending < _get_rock_limit(): rocks_pending += 1 await get_tree().create_timer(GameSettings.item_respawn_delay).timeout rocks_pending -= 1 if game_active: _spawn_single_rock() func _check_rock_slots() -> void: var gap := _get_rock_limit() - rocks_on_field - rocks_pending for i in range(gap): _spawn_single_rock() # ─── Player ─────────────────────────────────────────────────────────────────── func _spawn_player() -> void: player = PLAYER_SCENE.instantiate() as CharacterBody3D player.position = Vector3(0, 0, 0) player.connect("died", _on_player_died) player.connect("health_changed", _on_health_changed) add_child(player) # ─── Game flow ──────────────────────────────────────────────────────────────── func _start_game() -> void: SFX.start_ambient(self) game_active = true wave = 1 score = 0 kills = 0 kills_for_next = 10 Enemy.first_leather_spawned = false Enemy.first_iron_spawned = false Enemy.first_essence_spawned = false _update_labels() spawn_timer.wait_time = GameSettings.enemy_spawn_interval spawn_timer.connect("timeout", _on_spawn_timer) spawn_timer.start() func _on_spawn_timer() -> void: if not game_active or upgrading: return _spawn_enemy() spawn_timer.wait_time = GameSettings.enemy_spawn_interval func _spawn_enemy() -> void: var enemy := ENEMY_SCENE.instantiate() as CharacterBody3D add_child(enemy) # Pick type based on wave var pool: Array[String] = ["slime"] if wave >= 4: pool.append("bat") if wave >= 7: pool.append("ogre") var type: String = pool[randi() % pool.size()] enemy.setup(type, wave) enemy.target = player enemy.connect("died", _on_enemy_died) enemy.connect("merged", _on_enemy_merged) if type == "bat" and not first_bat_spawned: first_bat_spawned = true show_tutorial("Tutorial_LeatherBoots") if type == "ogre" and not first_boss_spawned: first_boss_spawned = true _start_boss_phase() # Spawn at random edge var side := randi() % 4 var r := randf_range(-(arena_size - 1.0), arena_size - 1.0) match side: 0: enemy.position = Vector3(r, 0, -(arena_size - 0.5)) 1: enemy.position = Vector3(r, 0, (arena_size - 0.5)) 2: enemy.position = Vector3(-(arena_size - 0.5), 0, r) 3: enemy.position = Vector3( (arena_size - 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 _update_labels() _update_progress() if kills >= kills_for_next: kills = 0 kills_for_next = int(kills_for_next * 1.6) wave += 1 _check_rock_slots() _show_upgrade() func _spawn_upgraded_enemy(pos: Vector3, type: String, level: int, w: int) -> CharacterBody3D: var enemy := ENEMY_SCENE.instantiate() as CharacterBody3D add_child(enemy) enemy.setup(type, w) enemy.target = player enemy.enemy_level = level enemy.kick_tier = level enemy.toughness_tier = level enemy.call("_update_label") enemy.call("_apply_mesh", level) enemy.global_position = pos enemy.connect("died", _on_enemy_died) enemy.connect("merged", _on_enemy_merged) var tw := enemy.create_tween() var s: float = 1.0 + level * 0.3 tw.tween_property(enemy.mesh_node, "scale", Vector3(s, s, s), 0.2) var col_shape := enemy.get_node_or_null("CollisionShape3D") as CollisionShape3D if col_shape != null and col_shape.shape != null: var s3d: BoxShape3D = col_shape.shape as BoxShape3D var old_size: Vector3 = s3d.size col_shape.shape = BoxShape3D.new() (col_shape.shape as BoxShape3D).size = old_size * s if level >= 3 and not first_boss_spawned: first_boss_spawned = true _start_boss_phase() return enemy func _on_enemy_merged(_upgrade: bool) -> void: pass 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() func _on_health_changed(cur: int, mx: int) -> void: hp_bar.size.x = 200.0 * float(cur) / float(mx) # ─── Upgrades ───────────────────────────────────────────────────────────────── const UPGRADES := [ {"id": "kick_force", "name": "💥 Stronger Kick", "desc": "Enemies fly farther"}, {"id": "kick_range", "name": "🌐 Wider Kick", "desc": "Bigger kick area"}, {"id": "kick_cooldown", "name": "⚡ Faster Kick", "desc": "Kick more often"}, {"id": "move_speed", "name": "💨 Fleet Foot", "desc": "Move faster"}, {"id": "max_health", "name": "❤️ Vitality", "desc": "+30 max HP"}, ] func _show_upgrade() -> void: upgrading = true get_tree().paused = true upgrade_panel.visible = true # Pick 3 random upgrades var pool: Array = UPGRADES.duplicate() pool.shuffle() var choices: Array = pool.slice(0, 3) for i in range(3): var btn := upgrade_panel.get_node("VBox/Btn%d" % i) as Button if i < choices.size(): var upg: Dictionary = choices[i] btn.text = "%s\n%s" % [upg["name"], upg["desc"]] btn.visible = true # Disconnect old signals for conn in btn.get_signal_connection_list("pressed"): btn.disconnect("pressed", conn["callable"]) var uid: String = upg["id"] btn.connect("pressed", _pick_upgrade.bind(uid)) else: btn.visible = false func _pick_upgrade(id: String) -> void: player.apply_upgrade(id) upgrade_panel.visible = false get_tree().paused = false upgrading = false _update_labels() func _show_gameover() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE var lbl := gameover_panel.get_node("VBox/ScoreLabel") as Label lbl.text = "Score: %d\nWave: %d" % [score, wave] show_tutorial("LoseGame", func() -> void: gameover_panel.visible = true ) 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 = 120.0 _spawn_portal() show_tutorial("ThirdLevelEnemy", func() -> void: boss_timer_label.visible = true boss_hint_label.visible = true ) 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() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE (win_panel.get_node("VBox/ScoreLabel") as Label).text = "Score: %d\nWave: %d" % [score, wave] show_tutorial("VictoryScreen", func() -> void: get_tree().paused = true win_panel.visible = true ) 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: tutorial_canvas = CanvasLayer.new() tutorial_canvas.process_mode = Node.PROCESS_MODE_ALWAYS tutorial_canvas.visible = false add_child(tutorial_canvas) var bg := ColorRect.new() bg.color = Color(0.0, 0.0, 0.0, 0.78) bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) tutorial_canvas.add_child(bg) tutorial_image = TextureRect.new() tutorial_image.anchor_left = 0.02 tutorial_image.anchor_right = 0.98 tutorial_image.anchor_top = 0.02 tutorial_image.anchor_bottom = 0.91 tutorial_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED tutorial_image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE tutorial_canvas.add_child(tutorial_image) tutorial_hint = Label.new() tutorial_hint.anchor_left = 0.5 tutorial_hint.anchor_right = 0.5 tutorial_hint.anchor_top = 0.93 tutorial_hint.offset_left = -280 tutorial_hint.offset_right = 280 tutorial_hint.text = "Нажмите ЛКМ чтобы продолжить" tutorial_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER tutorial_hint.add_theme_font_size_override("font_size", 22) tutorial_hint.add_theme_color_override("font_color", Color.WHITE) tutorial_hint.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.9)) tutorial_hint.add_theme_constant_override("shadow_offset_x", 2) tutorial_hint.add_theme_constant_override("shadow_offset_y", 2) tutorial_hint.visible = false tutorial_canvas.add_child(tutorial_hint) var click_cap := Control.new() click_cap.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) click_cap.mouse_filter = Control.MOUSE_FILTER_STOP click_cap.process_mode = Node.PROCESS_MODE_ALWAYS click_cap.connect("gui_input", _on_tutorial_input) tutorial_canvas.add_child(click_cap) func _on_tutorial_input(event: InputEvent) -> void: if not tutorial_hint_ready: return var mb := event as InputEventMouseButton if mb != null and mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: _dismiss_tutorial() func _set_enemies_paused(paused: bool) -> void: var mode := Node.PROCESS_MODE_DISABLED if paused else Node.PROCESS_MODE_PAUSABLE for e in get_tree().get_nodes_in_group("enemies"): (e as Node).process_mode = mode spawn_timer.paused = paused func _set_player_paused(paused: bool) -> void: var mode := Node.PROCESS_MODE_DISABLED if paused else Node.PROCESS_MODE_PAUSABLE for e in get_tree().get_nodes_in_group("player"): (e as Node).process_mode = mode const _REPEATABLE_TUTORIALS := ["VictoryScreen", "LoseGame"] func show_tutorial(key: String, on_dismiss: Callable = Callable()) -> void: if shown_tutorials.get(key, false) and not key in _REPEATABLE_TUTORIALS: if on_dismiss.is_valid(): on_dismiss.call() return shown_tutorials[key] = true var path := "res://assets/%s.jpeg" % key tutorial_image.texture = load(path) if ResourceLoader.exists(path) else null tutorial_on_dismiss = on_dismiss tutorial_hint_ready = false tutorial_hint.visible = false tutorial_canvas.visible = true tutorial_active = true _set_enemies_paused(true) _set_player_paused(true) await get_tree().create_timer(3.0).timeout tutorial_hint.visible = true tutorial_hint_ready = true func _dismiss_tutorial() -> void: tutorial_canvas.visible = false tutorial_active = false _set_enemies_paused(false) _set_player_paused(false) if tutorial_on_dismiss.is_valid(): tutorial_on_dismiss.call() # ─── UI ─────────────────────────────────────────────────────────────────────── func _create_ui() -> void: canvas = CanvasLayer.new() canvas.process_mode = Node.PROCESS_MODE_ALWAYS add_child(canvas) _make_hud() _make_upgrade_panel() _make_gameover_panel() _make_boss_ui() _make_win_panel() _make_pause_panel() func _make_hud() -> void: score_label = _label(Vector2(16, 12), "Score: 0", 30) wave_label = _label(Vector2(16, 50), "Wave: 1", 30) _make_recipe_panel() func _make_recipe_panel() -> void: const INGREDIENT_NAMES := { "rock": "Rock", "stick": "Stick", "leather": "Leather", "iron": "Iron", "metal_plate":"Iron Plate", #"MetalPlate":"Armor boots" "essence":"Essence", "Enchanted_Table": "Ench Table", "EnchantedSphere": "Enchanted Boots" } const RESULT_NAMES := { "LeatherBoots": "Leather Boots", "Boulder": "Boulder", "StickArmor": "Stick Armor", "LeatherArmor": "Leather Armor", "PlateArmor": "Plate Armor", "WoodenShield": "Wooden Shield", "IronShield": "Iron Shield", "MetalArmor": "Metal Armor", "MetalPlate": "Armor boots" } const PAD_H := 10 const PAD_V := 8 const SEP := 4 const TITLE_H := 22 const ROW_H := 20 var n := MergeRecipes._list.size() var content_h := TITLE_H + SEP + n * ROW_H + (n - 1) * SEP var panel_h := content_h + PAD_V * 2 + 10 const PANEL_W := 260 var panel := Panel.new() var sb := StyleBoxFlat.new() sb.bg_color = Color(0.0, 0.0, 0.0, 0.55) sb.corner_radius_top_left = 6 sb.corner_radius_top_right = 6 sb.corner_radius_bottom_left = 6 sb.corner_radius_bottom_right = 6 panel.add_theme_stylebox_override("panel", sb) panel.anchor_left = 1.0 panel.anchor_right = 1.0 panel.anchor_top = 0.0 panel.anchor_bottom = 0.0 panel.offset_left = -(PANEL_W + 10) panel.offset_right = -10 panel.offset_top = 10 panel.offset_bottom = 10 + panel_h canvas.add_child(panel) var margin := MarginContainer.new() margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) margin.add_theme_constant_override("margin_left", PAD_H) margin.add_theme_constant_override("margin_right", PAD_H) margin.add_theme_constant_override("margin_top", PAD_V) margin.add_theme_constant_override("margin_bottom", PAD_V) panel.add_child(margin) var vbox := VBoxContainer.new() vbox.add_theme_constant_override("separation", SEP) margin.add_child(vbox) var title := Label.new() title.text = "Рецепты" title.add_theme_font_size_override("font_size", 17) title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.5)) title.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8)) title.add_theme_constant_override("shadow_offset_x", 1) title.add_theme_constant_override("shadow_offset_y", 1) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(title) for recipe in MergeRecipes._list: var a: String = INGREDIENT_NAMES.get(recipe["ingredients"][0], recipe["ingredients"][0]) var b: String = INGREDIENT_NAMES.get(recipe["ingredients"][1], recipe["ingredients"][1]) var scene_name: String = (recipe["result_scene"] as String).get_file().get_basename() var result: String = RESULT_NAMES.get(scene_name, scene_name) var row := Label.new() row.text = "%s + %s → %s" % [a, b, result] row.add_theme_font_size_override("font_size", 15) row.add_theme_color_override("font_color", Color.WHITE) row.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8)) row.add_theme_constant_override("shadow_offset_x", 1) row.add_theme_constant_override("shadow_offset_y", 1) vbox.add_child(row) _label(Vector2(16, 92), "HP", 20) hp_bar_bg = _crect(Vector2(16, 118), Vector2(260, 20), Color(0.25, 0.04, 0.04)) hp_bar = _crect(Vector2(16, 118), Vector2(260, 20), Color(0.9, 0.15, 0.15)) _label(Vector2(16, 146), "Next upgrade", 20) progress_bg = _crect(Vector2(16, 172), Vector2(260, 14), Color(0.1, 0.1, 0.25)) progress_bar = _crect(Vector2(16, 172), Vector2(0, 14), Color(0.4, 0.8, 1.0)) tier_label = _label(Vector2(16, 194), "", 20) _make_equipment_slots() func _make_upgrade_panel() -> void: upgrade_panel = Panel.new() upgrade_panel.process_mode = Node.PROCESS_MODE_ALWAYS upgrade_panel.visible = false canvas.add_child(upgrade_panel) # Center with explicit anchors (420x300) upgrade_panel.anchor_left = 0.5 upgrade_panel.anchor_right = 0.5 upgrade_panel.anchor_top = 0.5 upgrade_panel.anchor_bottom = 0.5 upgrade_panel.offset_left = -210.0 upgrade_panel.offset_right = 210.0 upgrade_panel.offset_top = -150.0 upgrade_panel.offset_bottom = 150.0 var vbox := VBoxContainer.new() vbox.name = "VBox" vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) vbox.add_theme_constant_override("separation", 12) vbox.alignment = BoxContainer.ALIGNMENT_CENTER upgrade_panel.add_child(vbox) var title := Label.new() title.text = "LEVEL UP! Choose an upgrade:" title.add_theme_font_size_override("font_size", 20) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(title) for i in range(3): var btn := Button.new() btn.name = "Btn%d" % i btn.custom_minimum_size = Vector2(380, 60) btn.add_theme_font_size_override("font_size", 16) btn.process_mode = Node.PROCESS_MODE_ALWAYS vbox.add_child(btn) func _make_gameover_panel() -> void: gameover_panel = Panel.new() gameover_panel.process_mode = Node.PROCESS_MODE_ALWAYS gameover_panel.visible = false canvas.add_child(gameover_panel) # Center with explicit anchors (360x240) gameover_panel.anchor_left = 0.5 gameover_panel.anchor_right = 0.5 gameover_panel.anchor_top = 0.5 gameover_panel.anchor_bottom = 0.5 gameover_panel.offset_left = -180.0 gameover_panel.offset_right = 180.0 gameover_panel.offset_top = -120.0 gameover_panel.offset_bottom = 120.0 var vbox := VBoxContainer.new() vbox.name = "VBox" vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) vbox.alignment = BoxContainer.ALIGNMENT_CENTER vbox.add_theme_constant_override("separation", 16) gameover_panel.add_child(vbox) var title := Label.new() title.text = "GAME OVER" title.add_theme_font_size_override("font_size", 32) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(title) var score_lbl := Label.new() score_lbl.name = "ScoreLabel" score_lbl.add_theme_font_size_override("font_size", 20) score_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(score_lbl) var restart_btn := Button.new() restart_btn.text = "Play Again" restart_btn.add_theme_font_size_override("font_size", 18) restart_btn.process_mode = Node.PROCESS_MODE_ALWAYS 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 := 64 const _EQUIP_SLOT_GAP := 10 const _EQUIP_SLOT_PAD := 3 const _EQUIP_START_X := 16 const _EQUIP_START_Y := 228 const _EQUIP_EMPTY_COL := Color(0.10, 0.10, 0.16) const _EQUIP_BORDER_COL := Color(0.30, 0.30, 0.42) # [slot_index][tier] = [fill_color, label_text] const _EQUIP_DATA := [ [ # shield [Color(0.10, 0.10, 0.16), ""], [Color(0.55, 0.38, 0.18), "Wood\nShield"], [Color(0.55, 0.58, 0.62), "Iron\nShield"], ], [ # armor (toughness_tier) [Color(0.10, 0.10, 0.16), ""], [Color(0.76, 0.47, 0.18), "Leather\nArmor"], [Color(0.58, 0.68, 1.00), "Metal\nArmor"], ], [ # boots (kick_tier) [Color(0.10, 0.10, 0.16), ""], [Color(0.76, 0.47, 0.18), "Leather\nBoots"], [Color(0.58, 0.68, 1.00), "Plate\nBoots"], [Color(0.75, 0.18, 1.00), "Magic\nBoots"], ], ] func _make_equipment_slots() -> void: var titles := ["Shield", "Armor", "Boots"] for i in range(3): var x := _EQUIP_START_X + i * (_EQUIP_SLOT_SIZE + _EQUIP_SLOT_GAP) var y := _EQUIP_START_Y var title := _label(Vector2(x, y), titles[i], 16) title.size.x = _EQUIP_SLOT_SIZE title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER var border := ColorRect.new() border.position = Vector2(x, y + 22) border.size = Vector2(_EQUIP_SLOT_SIZE, _EQUIP_SLOT_SIZE) border.color = _EQUIP_BORDER_COL canvas.add_child(border) var fill := ColorRect.new() fill.position = Vector2(x + _EQUIP_SLOT_PAD, y + 22 + _EQUIP_SLOT_PAD) fill.size = Vector2(_EQUIP_SLOT_SIZE - _EQUIP_SLOT_PAD * 2, _EQUIP_SLOT_SIZE - _EQUIP_SLOT_PAD * 2) fill.color = _EQUIP_EMPTY_COL canvas.add_child(fill) equip_fills.append(fill) var lbl := Label.new() lbl.position = fill.position lbl.size = fill.size lbl.text = "" lbl.add_theme_font_size_override("font_size", 14) lbl.add_theme_color_override("font_color", Color.WHITE) lbl.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.85)) lbl.add_theme_constant_override("shadow_offset_x", 1) lbl.add_theme_constant_override("shadow_offset_y", 1) lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART canvas.add_child(lbl) equip_labels.append(lbl) func _update_equipment_slots() -> void: if not is_instance_valid(player): return var tiers := [ player.get("shield_tier") as int, player.get("toughness_tier") as int, player.get("kick_tier") as int, ] for i in range(3): var tier: int = tiers[i] if tier == _equip_prev_tiers[i]: continue _equip_prev_tiers[i] = tier var data: Array = _EQUIP_DATA[i] var entry: Array = data[clampi(tier, 0, data.size() - 1)] equip_fills[i].color = entry[0] as Color equip_labels[i].text = entry[1] as String if tier > 0: var tw := create_tween() tw.tween_property(equip_fills[i], "color", Color.WHITE, 0.06) tw.tween_property(equip_fills[i], "color", entry[0] as Color, 0.22) # ─── UI helpers ─────────────────────────────────────────────────────────────── func _label(pos: Vector2, text: String, size: int) -> Label: var lbl := Label.new() lbl.position = pos lbl.text = text lbl.add_theme_font_size_override("font_size", size) lbl.add_theme_color_override("font_color", Color.WHITE) lbl.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8)) lbl.add_theme_constant_override("shadow_offset_x", 2) lbl.add_theme_constant_override("shadow_offset_y", 2) canvas.add_child(lbl) return lbl func _crect(pos: Vector2, sz: Vector2, col: Color) -> ColorRect: var r := ColorRect.new() r.position = pos r.size = sz r.color = col canvas.add_child(r) return r func _update_labels() -> void: score_label.text = "Score: %d" % score wave_label.text = "Wave: %d" % wave func _update_progress() -> void: var t := float(kills) / float(kills_for_next) progress_bar.size.x = 200.0 * t func _update_tier_label() -> void: if not is_instance_valid(player): return var kt: int = player.get("kick_tier") var tt: int = player.get("toughness_tier") var st: int = player.get("shield_tier") var shield_str := "-" if st == 0 else str(st) tier_label.text = "Kick: %d Tough: %d Shield: %s" % [kt, tt, shield_str] _update_equipment_slots() # ─── Pause menu ─────────────────────────────────────────────────────────────── const _PAUSE_CRAFT_IMG := "res://assets/Pause_Craft.jpeg" const _PAUSE_CONTROLS_IMG := "res://assets/Pause_Controls.jpeg" func _make_pause_panel() -> void: pause_panel = Panel.new() pause_panel.process_mode = Node.PROCESS_MODE_ALWAYS pause_panel.visible = false var sb := StyleBoxFlat.new() sb.bg_color = Color(0.05, 0.04, 0.10, 0.96) sb.border_width_left = 2; sb.border_width_right = 2 sb.border_width_top = 2; sb.border_width_bottom = 2 sb.border_color = Color(0.35, 0.28, 0.55) sb.corner_radius_top_left = 10; sb.corner_radius_top_right = 10 sb.corner_radius_bottom_left = 10; sb.corner_radius_bottom_right = 10 pause_panel.add_theme_stylebox_override("panel", sb) pause_panel.anchor_left = 0.5; pause_panel.anchor_right = 0.5 pause_panel.anchor_top = 0.5; pause_panel.anchor_bottom = 0.5 pause_panel.offset_left = -380; pause_panel.offset_right = 380 pause_panel.offset_top = -300; pause_panel.offset_bottom = 300 canvas.add_child(pause_panel) var vbox := VBoxContainer.new() vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) vbox.add_theme_constant_override("separation", 12) var margin := MarginContainer.new() margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) margin.add_theme_constant_override("margin_left", 20) margin.add_theme_constant_override("margin_right", 20) margin.add_theme_constant_override("margin_top", 16) margin.add_theme_constant_override("margin_bottom", 16) pause_panel.add_child(margin) margin.add_child(vbox) var title := Label.new() title.text = "ПАУЗА" title.add_theme_font_size_override("font_size", 28) title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.5)) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(title) pause_image = TextureRect.new() pause_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED pause_image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE pause_image.custom_minimum_size = Vector2(0, 380) pause_image.size_flags_vertical = Control.SIZE_EXPAND_FILL vbox.add_child(pause_image) _pause_load_image() var hbox := HBoxContainer.new() hbox.alignment = BoxContainer.ALIGNMENT_CENTER hbox.add_theme_constant_override("separation", 16) vbox.add_child(hbox) pause_toggle_btn = _pause_btn("Управление", _on_pause_toggle) hbox.add_child(pause_toggle_btn) hbox.add_child(_pause_btn("Продолжить", _toggle_pause_menu)) hbox.add_child(_pause_btn("Главное меню", func() -> void: get_tree().paused = false get_tree().change_scene_to_file("res://scenes/MainMenu.tscn") )) func _pause_btn(text: String, cb: Callable) -> Button: var b := Button.new() b.text = text b.process_mode = Node.PROCESS_MODE_ALWAYS b.custom_minimum_size = Vector2(160, 48) b.add_theme_font_size_override("font_size", 17) b.connect("pressed", cb) return b func _pause_load_image() -> void: var path := _PAUSE_CRAFT_IMG if _pause_showing_craft else _PAUSE_CONTROLS_IMG pause_image.texture = load(path) as Texture2D if ResourceLoader.exists(path) else null func _on_pause_toggle() -> void: _pause_showing_craft = not _pause_showing_craft pause_toggle_btn.text = "Управление" if _pause_showing_craft else "Рецепты Крафта" _pause_load_image() func _toggle_pause_menu() -> void: var opening := not pause_panel.visible pause_panel.visible = opening get_tree().paused = opening Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if opening else Input.MOUSE_MODE_CAPTURED if opening: _pause_showing_craft = true pause_toggle_btn.text = "Управление" _pause_load_image()