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 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 := 10 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 # UI nodes var canvas: CanvasLayer var score_label: Label var wave_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 func _ready() -> void: _spawn_level() _create_camera() _create_ui() _spawn_player() _spawn_rocks() _start_game() add_to_group("main") Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func _input(event: InputEvent) -> void: 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 Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE else: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED # ─── 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) 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_size - 2.0) rock.position = Vector3(cos(angle) * dist, 0.0, sin(angle) * dist) # ─── 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: game_active = true wave = 1 score = 0 kills = 0 kills_for_next = 10 _update_labels() spawn_timer.wait_time = 1.4 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 = SPAWN_TIME # max(0.25, 1.4 - wave * 0.07) 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) # 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 _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 + 1 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 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) return enemy func _on_enemy_merged(_upgrade: bool) -> void: pass func _on_player_died() -> void: game_active = false 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: gameover_panel.visible = true var lbl := gameover_panel.get_node("VBox/ScoreLabel") as Label lbl.text = "Score: %d\nWave: %d" % [score, wave] func _restart() -> void: get_tree().paused = false get_tree().reload_current_scene() # ─── 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() func _make_hud() -> void: # Score score_label = _label(Vector2(12, 10), "Score: 0", 22) # Wave wave_label = _label(Vector2(12, 38), "Wave: 1", 22) # HP bar _label(Vector2(12, 68), "HP", 16) hp_bar_bg = _crect(Vector2(12, 88), Vector2(200, 16), Color(0.25, 0.04, 0.04)) hp_bar = _crect(Vector2(12, 88), Vector2(200, 16), Color(0.9, 0.15, 0.15)) # Kill progress toward next upgrade _label(Vector2(12, 110), "Next upgrade", 16) progress_bg = _crect(Vector2(12, 130), Vector2(200, 10), Color(0.1, 0.1, 0.25)) progress_bar = _crect(Vector2(12, 130), Vector2(0, 10), Color(0.4, 0.8, 1.0)) 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) # ─── 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