extends Node3D const PLAYER_SCENE := preload("res://scenes/Player.tscn") const ENEMY_SCENE := preload("res://scenes/Enemy.tscn") const ARENA := 14.0 const WALL_T := 1.2 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 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: _create_environment() _create_arena() _create_camera() _create_ui() _spawn_player() _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 # ─── Environment ────────────────────────────────────────────────────────────── func _create_environment() -> void: var env := Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = Color(0.04, 0.04, 0.08) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.35, 0.35, 0.5) env.ambient_light_energy = 0.6 var we := WorldEnvironment.new() we.environment = env add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-55, -25, 0) sun.light_energy = 1.8 sun.shadow_enabled = true add_child(sun) # ─── Arena ──────────────────────────────────────────────────────────────────── func _create_arena() -> void: var floor_mat := StandardMaterial3D.new() floor_mat.albedo_color = Color(0.12, 0.12, 0.18) floor_mat.roughness = 1.0 # Floor mesh (visual only) var fm := MeshInstance3D.new() var plane := PlaneMesh.new() plane.size = Vector2(ARENA * 2, ARENA * 2) plane.subdivide_width = 8 plane.subdivide_depth = 8 fm.mesh = plane fm.material_override = floor_mat add_child(fm) # Floor collider var fb := StaticBody3D.new() var fc := CollisionShape3D.new() var fs := BoxShape3D.new() fs.size = Vector3(ARENA * 2, 0.2, ARENA * 2) fc.shape = fs fb.position.y = -0.1 fb.add_child(fc) add_child(fb) # Grid lines on floor _draw_grid() # Four walls var wall_mat := StandardMaterial3D.new() wall_mat.albedo_color = Color(0.28, 0.28, 0.42) wall_mat.roughness = 0.9 wall_mat.metallic = 0.1 _make_wall(Vector3(0, 0.5, -(ARENA + WALL_T * 0.5)), Vector3(ARENA * 2 + WALL_T * 2, 1.0, WALL_T), wall_mat) _make_wall(Vector3(0, 0.5, (ARENA + WALL_T * 0.5)), Vector3(ARENA * 2 + WALL_T * 2, 1.0, WALL_T), wall_mat) _make_wall(Vector3(-(ARENA + WALL_T * 0.5), 0.5, 0), Vector3(WALL_T, 1.0, ARENA * 2), wall_mat) _make_wall(Vector3( (ARENA + WALL_T * 0.5), 0.5, 0), Vector3(WALL_T, 1.0, ARENA * 2), wall_mat) func _make_wall(pos: Vector3, size: Vector3, mat: StandardMaterial3D) -> void: var body := StaticBody3D.new() body.position = pos body.set_meta("is_wall", true) var col := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = size col.shape = shape body.add_child(col) var msh := MeshInstance3D.new() var box := BoxMesh.new() box.size = size msh.mesh = box msh.material_override = mat body.add_child(msh) add_child(body) func _draw_grid() -> void: # Subtle grid as thin quads var grid_mat := StandardMaterial3D.new() grid_mat.albedo_color = Color(0.2, 0.2, 0.3, 0.5) grid_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA grid_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED var step := 4.0 var n := int(ARENA / step) for i in range(-n, n + 1): for axis in [0, 1]: var msh := MeshInstance3D.new() var box := BoxMesh.new() if axis == 0: box.size = Vector3(0.05, 0.01, ARENA * 2) msh.position = Vector3(i * step, 0.005, 0) else: box.size = Vector3(ARENA * 2, 0.01, 0.05) msh.position = Vector3(0, 0.005, i * step) msh.mesh = box msh.material_override = grid_mat add_child(msh) # ─── 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 ─────────────────────────────────────────────────────────────────── 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 - 1.0), ARENA - 1.0) match side: 0: enemy.position = Vector3(r, 0, -(ARENA - 0.5)) 1: enemy.position = Vector3(r, 0, (ARENA - 0.5)) 2: enemy.position = Vector3(-(ARENA - 0.5), 0, r) 3: enemy.position = Vector3( (ARENA - 0.5), 0, r) 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