Добавить в корзинуПозвонить
Найти в Дзене
Skript’S

Скрипт для шаблона игры в жанре Tower Defence. Чтобы активировать нужно в Node3D вставить этот скрипт. И всё работает!

# ==============================================================================
# TOWER DEFENSE — Полноценный профессиональный скрипт для Godot 4
# Вставь этот скрипт на корневой узел Node3D твоей сцены.
#

# ==============================================================================

# TOWER DEFENSE — Полноценный профессиональный скрипт для Godot 4

# Вставь этот скрипт на корневой узел Node3D твоей сцены.

#

# ЧТО НУЖНО В СЦЕНЕ:

# - Node3D (корень) — этот скрипт

# - Camera3D (имя: "Camera3D") — настрой вид сверху, угол ~60°

# - DirectionalLight3D    — освещение

# - MeshInstance3D "Ground"  — плоскость земли (PlaneMesh 40x40)

# - Area3D + CollisionShape3D "PlacementArea" — вся карта для кликов

# - Node3D "Towers"      — контейнер башен

# - Node3D "Enemies"     — контейнер врагов

# - Node3D "Projectiles"   — контейнер снарядов

# - Node3D "Effects"     — контейнер эффектов

# - CanvasLayer "UI" с Control — для HUD (создаётся автоматически кодом)

#

# ПУТЬ ВРАГОВ: задаётся массивом PATH_WAYPOINTS ниже.

# ЗАМЕНА МОДЕЛЕЙ: ищи функции _create_*_mesh() и замени MeshInstance3D

#         на load("res://твоя_модель.glb")

# ==============================================================================

extends Node3D

# ─── НАСТРОЙКИ ПУТИ (мировые координаты XZ) ────────────────────────────────

const PATH_WAYPOINTS: Array[Vector3] = [

Vector3(-18, 0, -18),

Vector3(-18, 0, -6),

Vector3( -6, 0, -6),

Vector3( -6, 0, 6),

Vector3( 6, 0, 6),

Vector3( 6, 0, -6),

Vector3( 18, 0, -6),

Vector3( 18, 0, 18),

]

# ─── НАСТРОЙКИ БАШЕН ────────────────────────────────────────────────────────

const TOWER_DEFS: Dictionary = {

"basic": {

"name":   "Стрелок",

"cost":   50,

"damage":  20,

"range":  6.0,

"fire_rate": 1.2,

"speed":  18.0,

"color":  Color(0.2, 0.6, 1.0),

"proj_color":Color(0.4, 0.8, 1.0),

"desc":   "Базовая башня. Дешёво и сердито.",

"sell":   25,

},

"sniper": {

"name":   "Снайпер",

"cost":   120,

"damage":  80,

"range":  14.0,

"fire_rate": 2.8,

"speed":  30.0,

"color":  Color(0.9, 0.7, 0.1),

"proj_color":Color(1.0, 0.9, 0.3),

"desc":   "Дальнобойная. Медленная, но мощная.",

"sell":   60,

},

"rapid": {

"name":   "Пулемёт",

"cost":   90,

"damage":  8,

"range":  5.0,

"fire_rate": 0.18,

"speed":  22.0,

"color":  Color(1.0, 0.3, 0.2),

"proj_color":Color(1.0, 0.5, 0.2),

"desc":   "Очень быстрая стрельба, малый урон.",

"sell":   45,

},

"frost": {

"name":   "Мороз",

"cost":   110,

"damage":  15,

"range":  6.5,

"fire_rate": 1.5,

"speed":  14.0,

"color":  Color(0.3, 0.9, 0.9),

"proj_color":Color(0.6, 1.0, 1.0),

"desc":   "Замедляет врагов на 50%.",

"sell":   55,

"slow":   0.5,

"slow_dur": 2.0,

},

"mortar": {

"name":   "Миномёт",

"cost":   200,

"damage":  120,

"range":  10.0,

"fire_rate": 4.0,

"speed":  12.0,

"color":  Color(0.5, 0.3, 0.8),

"proj_color":Color(0.8, 0.4, 1.0),

"desc":   "Взрывной урон по области (радиус 3).",

"sell":   100,

"splash":  3.0,

},

}

# ─── НАСТРОЙКИ ВРАГОВ ───────────────────────────────────────────────────────

const ENEMY_DEFS: Dictionary = {

"grunt": {

"name": "Пехотинец",

"hp":  80,

"speed": 3.5,

"reward": 10,

"color": Color(0.2, 0.8, 0.2),

"size": 0.5,

"armor": 0.0,

},

"heavy": {

"name": "Танк",

"hp":  400,

"speed": 2.0,

"reward": 30,

"color": Color(0.5, 0.5, 0.5),

"size": 0.8,

"armor": 0.3,

},

"fast": {

"name": "Разведчик",

"hp":  50,

"speed": 7.0,

"reward": 15,

"color": Color(1.0, 0.8, 0.1),

"size": 0.35,

"armor": 0.0,

},

"boss": {

"name": "Босс",

"hp":  2000,

"speed": 1.8,

"reward": 150,

"color": Color(0.9, 0.1, 0.1),

"size": 1.2,

"armor": 0.4,

},

"swarm": {

"name": "Рой",

"hp":  25,

"speed": 5.5,

"reward": 5,

"color": Color(0.8, 0.4, 0.9),

"size": 0.3,

"armor": 0.0,

},

}

# ─── ВОЛНЫ ──────────────────────────────────────────────────────────────────

const WAVES: Array = [

[{"type":"grunt", "count":8, "interval":1.0}],

[{"type":"grunt", "count":10, "interval":0.8}, {"type":"fast", "count":4, "interval":1.5}],

[{"type":"heavy", "count":3, "interval":2.5}, {"type":"grunt","count":8, "interval":0.8}],

[{"type":"fast", "count":12, "interval":0.4}],

[{"type":"grunt", "count":6, "interval":0.7}, {"type":"heavy","count":4,"interval":2.0}, {"type":"fast","count":6,"interval":0.6}],

[{"type":"swarm", "count":20, "interval":0.25}],

[{"type":"heavy", "count":6, "interval":1.8}, {"type":"swarm","count":15,"interval":0.3}],

[{"type":"boss", "count":1, "interval":5.0}, {"type":"grunt","count":10,"interval":0.6}],

[{"type":"fast", "count":15, "interval":0.35},{"type":"heavy","count":5,"interval":1.5}],

[{"type":"boss", "count":2, "interval":8.0}, {"type":"swarm","count":25,"interval":0.2}, {"type":"heavy","count":8,"interval":1.2}],

]

# ─── ИГРОВЫЕ ПЕРЕМЕННЫЕ ─────────────────────────────────────────────────────

var gold:    int = 150

var lives:    int = 20

var score:    int = 0

var wave_index: int = 0

var game_state: String = "prepare"

var selected_tower_type: String = "basic"

var hovered_cell: Vector3i = Vector3i(-999, 0, -999)

var placed_towers: Dictionary = {}

var preview_node: Node3D = null

var can_place:  bool = false

# ─── ВНУТРЕННИЕ УЗЛЫ ────────────────────────────────────────────────────────

var towers_root:  Node3D

var enemies_root:  Node3D

var projectiles_root:Node3D

var effects_root:  Node3D

var ui_layer:    CanvasLayer

# ─── UI ЭЛЕМЕНТЫ ────────────────────────────────────────────────────────────

var lbl_gold:  Label

var lbl_lives: Label

var lbl_wave:  Label

var lbl_score: Label

var btn_start: Button

var panel_info: PanelContainer

var lbl_info:  Label

var bar_container: HBoxContainer

var tower_buttons: Dictionary = {}

var lbl_msg:  Label

var speed_btn: Button

var game_speed: float = 1.0

# ─── СПАВН ВОЛН ─────────────────────────────────────────────────────────────

var spawn_queue: Array = []

var wave_active: bool = false

var wave_timer:  float = 0.0

var between_timer: float = 0.0

const BETWEEN_WAVE_TIME: float = 8.0

# ─── PATH КЭШИ ──────────────────────────────────────────────────────────────

var path_cells: Array[Vector3i] = []

# ═══════════════════════════════════════════════════════════════════════════

func _ready() -> void:

Engine.max_fps = 120

_get_scene_nodes()

_build_path_visual()

_compute_path_cells()

_build_ui()

_create_preview()

_update_ui()

_show_message("TOWER DEFENSE\nВолна 1 из %d\nНажми НАЧАТЬ ВОЛНУ" % WAVES.size())

func _get_scene_nodes() -> void:

towers_root   = _get_or_create("Towers",   Node3D)

enemies_root  = _get_or_create("Enemies",  Node3D)

projectiles_root = _get_or_create("Projectiles", Node3D)

effects_root  = _get_or_create("Effects",  Node3D)

func _get_or_create(node_name: String, type) -> Node:

var n = get_node_or_null(node_name)

if n == null:

n = type.new()

n.name = node_name

add_child(n)

return n

# ═══════════════════════════════════════════════════════════════════════════

func _process(delta: float) -> void:

var d = delta * game_speed

if game_state == "wave":

_process_spawning(d)

_process_towers(d)

_process_projectiles(d)

_process_enemies(d)

_check_wave_done()

elif game_state == "prepare":

_process_towers(d)

_process_projectiles(d)

_process_enemies(d)

_process_effects(d)

_update_preview()

# ═══════════════════════════════════════════════════════════════════════════

func _start_wave() -> void:

if wave_index >= WAVES.size(): return

game_state = "wave"

wave_active = true

spawn_queue.clear()

btn_start.visible = false

lbl_msg.visible = false

var wave_def = WAVES[wave_index]

var offset = 0.0

for group in wave_def:

for i in range(group["count"]):

spawn_queue.append({"type": group["type"], "delay": offset + i * group["interval"]})

offset += group["count"] * group["interval"] + 1.0

spawn_queue.sort_custom(func(a, b): return a["delay"] < b["delay"])

wave_timer = 0.0

func _process_spawning(delta: float) -> void:

if spawn_queue.is_empty(): return

wave_timer += delta

while not spawn_queue.is_empty() and spawn_queue[0]["delay"] <= wave_timer:

_spawn_enemy(spawn_queue.pop_front()["type"])

func _spawn_enemy(type: String) -> void:

var def = ENEMY_DEFS[type]

var node = Node3D.new()

node.name = "Enemy_" + type

node.global_position = PATH_WAYPOINTS[0]

enemies_root.add_child(node)

var mesh_inst = _create_enemy_mesh(def)

node.add_child(mesh_inst)

var hp_bar = _create_hp_bar()

node.add_child(hp_bar)

node.set_meta("type",    type)

node.set_meta("hp",     int(def["hp"]))

node.set_meta("max_hp",   int(def["hp"]))

node.set_meta("speed",   float(def["speed"]))

node.set_meta("base_speed", float(def["speed"]))

node.set_meta("reward",   int(def["reward"]))

node.set_meta("armor",   float(def["armor"]))

node.set_meta("wp_index",  1)

node.set_meta("slow_timer", 0.0)

node.set_meta("hp_bar",   hp_bar)

node.set_meta("alive",   true)

func _process_enemies(delta: float) -> void:

for enemy in enemies_root.get_children():

if not enemy.get_meta("alive", false): continue

var wp_idx: int = enemy.get_meta("wp_index")

if wp_idx >= PATH_WAYPOINTS.size():

lives -= 1

_update_ui()

_flash_damage_ui()

enemy.set_meta("alive", false)

enemy.queue_free()

if lives <= 0: _game_over()

continue

var slow_t: float = enemy.get_meta("slow_timer")

if slow_t > 0.0:

enemy.set_meta("slow_timer", slow_t - delta)

enemy.set_meta("speed", enemy.get_meta("base_speed") * 0.5)

else:

enemy.set_meta("speed", enemy.get_meta("base_speed"))

var target_wp = PATH_WAYPOINTS[wp_idx]

var dir = (target_wp - enemy.global_position)

dir.y = 0.0

var dist = dir.length()

var spd: float = enemy.get_meta("speed")

if dist < 0.1:

enemy.set_meta("wp_index", wp_idx + 1)

else:

var move = dir.normalized() * spd * delta

enemy.global_position += move

if dir.length() > 0.01:

var look_target = enemy.global_position + dir.normalized()

look_target.y = enemy.global_position.y

enemy.look_at(look_target, Vector3.UP)

_update_enemy_hp_bar(enemy)

func _check_wave_done() -> void:

if not spawn_queue.is_empty(): return

if enemies_root.get_child_count() > 0:

var alive = false

for e in enemies_root.get_children():

if e.get_meta("alive", false): alive = true; break

if alive: return

wave_index += 1

if wave_index >= WAVES.size():

_victory()

return

game_state = "prepare"

wave_active = false

var bonus = 50 + wave_index * 10

gold += bonus

score += bonus

_update_ui()

btn_start.visible = true

btn_start.text  = "▶ ВОЛНА %d / %d" % [wave_index + 1, WAVES.size()]

_show_message("Волна %d пройдена!\n+%d золота\nСледующая волна: %d / %d" % [

wave_index, bonus, wave_index + 1, WAVES.size()])

# ═══════════════════════════════════════════════════════════════════════════

func _place_tower(cell: Vector3i) -> void:

if placed_towers.has(cell): return

if _is_path_cell(cell): return

var def = TOWER_DEFS[selected_tower_type]

if gold < def["cost"]:

_show_message("Недостаточно золота!", 1.5)

return

gold -= def["cost"]

var node = Node3D.new()

node.name = "Tower_" + selected_tower_type

node.global_position = Vector3(cell.x + 0.5, 0.0, cell.z + 0.5)

towers_root.add_child(node)

var mesh_inst = _create_tower_mesh(def)

node.add_child(mesh_inst)

var range_ring = _create_range_ring(def["range"], def["color"])

range_ring.visible = false

node.add_child(range_ring)

node.set_meta("tower_type", selected_tower_type)

node.set_meta("damage",   int(def["damage"]))

node.set_meta("range",   float(def["range"]))

node.set_meta("fire_rate", float(def["fire_rate"]))

node.set_meta("fire_timer", 0.0)

node.set_meta("speed",   float(def["speed"]))

node.set_meta("color",   def["color"])

node.set_meta("proj_color", def["proj_color"])

node.set_meta("slow",    def.get("slow", 0.0))

node.set_meta("slow_dur",  def.get("slow_dur", 0.0))

node.set_meta("splash",   def.get("splash", 0.0))

node.set_meta("sell",    int(def["sell"]))

node.set_meta("range_ring", range_ring)

node.set_meta("kills",   0)

node.set_meta("total_dmg", 0)

placed_towers[cell] = node

score += 2

_update_ui()

_spawn_place_effect(node.global_position, def["color"])

func _process_towers(delta: float) -> void:

for tower in towers_root.get_children():

var ft: float = tower.get_meta("fire_timer") - delta

tower.set_meta("fire_timer", ft)

if ft > 0.0: continue

var rng: float = tower.get_meta("range")

var target = _find_target(tower.global_position, rng)

if target == null: continue

tower.set_meta("fire_timer", tower.get_meta("fire_rate"))

var look_pos = target.global_position

look_pos.y = tower.global_position.y

tower.look_at(look_pos, Vector3.UP)

_fire(tower, target)

func _find_target(from: Vector3, range_val: float) -> Node3D:

var best:   Node3D = null

var best_prog: float = -1.0

for enemy in enemies_root.get_children():

if not enemy.get_meta("alive", false): continue

var d = from.distance_to(enemy.global_position)

if d > range_val: continue

var prog = float(enemy.get_meta("wp_index")) + \

(1.0 - from.distance_to(PATH_WAYPOINTS[min(enemy.get_meta("wp_index"), PATH_WAYPOINTS.size()-1)]) / 20.0)

if prog > best_prog:

best_prog = prog

best   = enemy

return best

func _fire(tower: Node3D, target: Node3D) -> void:

var proj = Node3D.new()

proj.name = "Proj"

proj.global_position = tower.global_position + Vector3(0, 0.8, 0)

projectiles_root.add_child(proj)

var pc: Color = tower.get_meta("proj_color")

var mesh_inst = MeshInstance3D.new()

var sph   = SphereMesh.new()

sph.radius  = 0.18

sph.height  = 0.36

mesh_inst.mesh = sph

var mat = StandardMaterial3D.new()

mat.albedo_color   = pc

mat.emission_enabled = true

mat.emission     = pc

mat.emission_energy_multiplier = 2.0

mesh_inst.material_override = mat

proj.add_child(mesh_inst)

proj.set_meta("target",  target)

proj.set_meta("damage",  tower.get_meta("damage"))

proj.set_meta("speed",  tower.get_meta("speed"))

proj.set_meta("slow",   tower.get_meta("slow"))

proj.set_meta("slow_dur", tower.get_meta("slow_dur"))

proj.set_meta("splash",  tower.get_meta("splash"))

proj.set_meta("tower_ref", tower)

proj.set_meta("alive",  true)

func _process_projectiles(delta: float) -> void:

for proj in projectiles_root.get_children():

if not proj.get_meta("alive", true): continue

var target = proj.get_meta("target")

if not is_instance_valid(target) or not target.get_meta("alive", false):

proj.set_meta("alive", false)

proj.queue_free()

continue

var dir = (target.global_position + Vector3(0, 0.5, 0)) - proj.global_position

var dist = dir.length()

var spd: float = proj.get_meta("speed")

if dist < 0.4:

_on_hit(proj, target)

proj.set_meta("alive", false)

proj.queue_free()

else:

proj.global_position += dir.normalized() * spd * delta

func _on_hit(proj: Node3D, target: Node3D) -> void:

var dmg:  int = proj.get_meta("damage")

var splash: float = proj.get_meta("splash")

var slow: float = proj.get_meta("slow")

var slow_d: float = proj.get_meta("slow_dur")

var tower    = proj.get_meta("tower_ref")

_spawn_hit_effect(target.global_position, proj.get_meta("slow") > 0.0)

if splash > 0.0:

_spawn_explosion_effect(target.global_position)

for enemy in enemies_root.get_children():

if not enemy.get_meta("alive", false): continue

if enemy.global_position.distance_to(target.global_position) <= splash:

_deal_damage(enemy, dmg, tower, slow, slow_d)

else:

_deal_damage(target, dmg, tower, slow, slow_d)

func _deal_damage(enemy: Node3D, dmg: int, tower: Node3D, slow: float, slow_d: float) -> void:

if not enemy.get_meta("alive", false): return

var armor:  float = enemy.get_meta("armor")

var real_dmg: int = max(1, int(dmg * (1.0 - armor)))

var hp:   int = enemy.get_meta("hp") - real_dmg

enemy.set_meta("hp", hp)

if is_instance_valid(tower):

tower.set_meta("total_dmg", tower.get_meta("total_dmg") + real_dmg)

if slow > 0.0:

enemy.set_meta("slow_timer", slow_d)

if hp <= 0:

_kill_enemy(enemy, tower)

else:

_show_damage_number(enemy.global_position, real_dmg, slow > 0.0)

func _kill_enemy(enemy: Node3D, tower: Node3D) -> void:

if not enemy.get_meta("alive", false): return

enemy.set_meta("alive", false)

var reward: int = enemy.get_meta("reward")

gold += reward

score += reward * 3

if is_instance_valid(tower):

tower.set_meta("kills", tower.get_meta("kills") + 1)

_update_ui()

_spawn_death_effect(enemy.global_position, ENEMY_DEFS[enemy.get_meta("type")]["color"])

enemy.queue_free()

# ═══════════════════════════════════════════════════════════════════════════

func _input(event: InputEvent) -> void:

if game_state == "gameover" or game_state == "victory": return

if event is InputEventMouseButton and event.pressed:

if event.button_index == MOUSE_BUTTON_LEFT:

var cell = _mouse_to_cell()

if cell != Vector3i(-999, 0, -999):

_place_tower(cell)

elif event.button_index == MOUSE_BUTTON_RIGHT:

var cell = _mouse_to_cell()

if placed_towers.has(cell):

_sell_tower(cell)

func _sell_tower(cell: Vector3i) -> void:

var tower = placed_towers[cell]

var refund: int = tower.get_meta("sell")

gold += refund

score += 1

placed_towers.erase(cell)

_spawn_place_effect(tower.global_position, Color(1.0, 0.8, 0.0))

tower.queue_free()

_update_ui()

# ═══════════════════════════════════════════════════════════════════════════

func _create_preview() -> void:

preview_node = Node3D.new()

preview_node.name = "Preview"

add_child(preview_node)

var mesh_inst = MeshInstance3D.new()

mesh_inst.name = "PreviewMesh"

var cyl = CylinderMesh.new()

cyl.top_radius  = 0.4

cyl.bottom_radius = 0.45

cyl.height    = 1.0

mesh_inst.mesh  = cyl

preview_node.add_child(mesh_inst)

var rng_ring = _create_range_ring(6.0, Color.WHITE)

rng_ring.name = "PreviewRange"

preview_node.add_child(rng_ring)

func _update_preview() -> void:

if preview_node == null: return

var cell = _mouse_to_cell()

if cell == Vector3i(-999, 0, -999):

preview_node.visible = false

return

preview_node.visible = true

preview_node.global_position = Vector3(cell.x + 0.5, 0.0, cell.z + 0.5)

can_place = not placed_towers.has(cell) and not _is_path_cell(cell)

var def = TOWER_DEFS[selected_tower_type]

var mesh_inst: MeshInstance3D = preview_node.get_node("PreviewMesh")

var mat = StandardMaterial3D.new()

if can_place and gold >= def["cost"]:

mat.albedo_color = Color(def["color"].r, def["color"].g, def["color"].b, 0.55)

else:

mat.albedo_color = Color(1.0, 0.2, 0.2, 0.45)

mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA

mesh_inst.material_override = mat

var rng_ring = preview_node.get_node_or_null("PreviewRange")

if rng_ring and rng_ring.get_meta("for_type", "") != selected_tower_type:

rng_ring.queue_free()

var new_ring = _create_range_ring(def["range"], def["color"])

new_ring.name = "PreviewRange"

new_ring.set_meta("for_type", selected_tower_type)

preview_node.add_child(new_ring)

# ═══════════════════════════════════════════════════════════════════════════

func _process_effects(delta: float) -> void:

for eff in effects_root.get_children():

var life: float = eff.get_meta("life", 0.0) - delta

if life <= 0.0:

eff.queue_free()

continue

eff.set_meta("life", life)

var total: float = eff.get_meta("total_life", 1.0)

var t = 1.0 - life / total

var vel: Vector3 = eff.get_meta("vel", Vector3.ZERO)

eff.global_position += vel * delta

vel.y -= 4.0 * delta

eff.set_meta("vel", vel)

var mesh = eff.get_node_or_null("M")

if mesh and mesh.material_override:

var a = (1.0 - t) * eff.get_meta("alpha", 1.0)

mesh.material_override.albedo_color.a = clamp(a, 0, 1)

func _spawn_hit_effect(pos: Vector3, is_slow: bool) -> void:

var n = Node3D.new()

n.global_position = pos + Vector3(0, 0.5, 0)

effects_root.add_child(n)

var m = MeshInstance3D.new(); m.name = "M"

var sph = SphereMesh.new(); sph.radius = 0.35; m.mesh = sph

var mat = StandardMaterial3D.new()

mat.albedo_color = Color(0.6, 1.0, 1.0, 0.9) if is_slow else Color(1.0, 0.8, 0.3, 0.9)

mat.emission_enabled = true; mat.emission = mat.albedo_color

mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA

m.material_override = mat

n.add_child(m)

n.set_meta("life", 0.18); n.set_meta("total_life", 0.18)

n.set_meta("vel", Vector3(randf_range(-1,1), 2.0, randf_range(-1,1)))

n.set_meta("alpha", 0.9)

func _spawn_death_effect(pos: Vector3, color: Color) -> void:

for i in range(8):

var n = Node3D.new()

n.global_position = pos + Vector3(0, 0.4, 0)

effects_root.add_child(n)

var m = MeshInstance3D.new(); m.name = "M"

var sph = SphereMesh.new(); sph.radius = 0.2; m.mesh = sph

var mat = StandardMaterial3D.new()

mat.albedo_color = color; mat.emission_enabled = true

mat.emission = color; mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA

m.material_override = mat; n.add_child(m)

n.set_meta("life", 0.5 + randf()*0.4); n.set_meta("total_life", 0.9)

n.set_meta("vel", Vector3(randf_range(-3,3), randf_range(2,5), randf_range(-3,3)))

n.set_meta("alpha", 1.0)

func _spawn_explosion_effect(pos: Vector3) -> void:

for i in range(12):

var n = Node3D.new()

n.global_position = pos + Vector3(0, 0.3, 0)

effects_root.add_child(n)

var m = MeshInstance3D.new(); m.name = "M"

var sph = SphereMesh.new(); sph.radius = 0.3; m.mesh = sph

var mat = StandardMaterial3D.new()

var c = Color(1.0, randf_range(0.3, 0.7), 0.1)

mat.albedo_color = c; mat.emission_enabled = true; mat.emission = c

mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA

m.material_override = mat; n.add_child(m)

n.set_meta("life", 0.6); n.set_meta("total_life", 0.6)

n.set_meta("vel", Vector3(randf_range(-4,4), randf_range(1,6), randf_range(-4,4)))

n.set_meta("alpha", 1.0)

func _spawn_place_effect(pos: Vector3, color: Color) -> void:

for i in range(6):

var n = Node3D.new()

n.global_position = pos + Vector3(0, 0.2, 0)

effects_root.add_child(n)

var m = MeshInstance3D.new(); m.name = "M"

var sph = SphereMesh.new(); sph.radius = 0.15; m.mesh = sph

var mat = StandardMaterial3D.new()

mat.albedo_color = color; mat.emission_enabled = true; mat.emission = color

mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA

m.material_override = mat; n.add_child(m)

n.set_meta("life", 0.45); n.set_meta("total_life", 0.45)

n.set_meta("vel", Vector3(randf_range(-2,2), randf_range(2,4), randf_range(-2,2)))

n.set_meta("alpha", 1.0)

var _dmg_labels: Array = []

func _show_damage_number(pos: Vector3, dmg: int, is_slow: bool) -> void:

var lbl = Label3D.new()

lbl.text   = str(dmg)

lbl.font_size = 48

lbl.modulate = Color(0.4, 0.9, 1.0) if is_slow else Color(1.0, 0.9, 0.3)

lbl.billboard = BaseMaterial3D.BILLBOARD_ENABLED

lbl.no_depth_test = true

lbl.global_position = pos + Vector3(randf_range(-0.3,0.3), 1.0, randf_range(-0.3,0.3))

effects_root.add_child(lbl)

var tween = create_tween()

tween.tween_property(lbl, "global_position", lbl.global_position + Vector3(0,1.5,0), 0.8)

tween.parallel().tween_property(lbl, "modulate:a", 0.0, 0.8)

tween.tween_callback(lbl.queue_free)

var _ui_flash_tween: Tween

func _flash_damage_ui() -> void:

if _ui_flash_tween: _ui_flash_tween.kill()

_ui_flash_tween = create_tween()

lbl_lives.modulate = Color(1, 0, 0)

_ui_flash_tween.tween_property(lbl_lives, "modulate", Color.WHITE, 0.5)

# ═══════════════════════════════════════════════════════════════════════════

func _create_tower_mesh(def: Dictionary) -> Node3D:

var root = Node3D.new()

var base_inst = MeshInstance3D.new()

var cyl = CylinderMesh.new()

cyl.top_radius = 0.3; cyl.bottom_radius = 0.45; cyl.height = 1.0

base_inst.mesh = cyl

var mat = StandardMaterial3D.new()

mat.albedo_color = def["color"]

mat.roughness = 0.4; mat.metallic = 0.6

base_inst.material_override = mat

base_inst.position = Vector3(0, 0.5, 0)

root.add_child(base_inst)

var top_inst = MeshInstance3D.new()

var sph = SphereMesh.new(); sph.radius = 0.28

top_inst.mesh = sph

var mat2 = StandardMaterial3D.new()

mat2.albedo_color = def["color"] * 1.3

mat2.emission_enabled = true; mat2.emission = def["color"]

mat2.emission_energy_multiplier = 0.6

mat2.roughness = 0.2; mat2.metallic = 0.8

top_inst.material_override = mat2

top_inst.position = Vector3(0, 1.15, 0)

root.add_child(top_inst)

return root

func _create_enemy_mesh(def: Dictionary) -> MeshInstance3D:

var m = MeshInstance3D.new()

var cap = CapsuleMesh.new()

cap.radius = def["size"] * 0.5

cap.height = def["size"] * 1.6

m.mesh = cap

var mat = StandardMaterial3D.new()

mat.albedo_color = def["color"]

mat.roughness = 0.5

m.material_override = mat

m.position = Vector3(0, def["size"] * 0.8, 0)

return m

func _create_hp_bar() -> Node3D:

var root = Node3D.new()

root.name = "HPBar"

root.position = Vector3(0, 2.0, 0)

var bg = MeshInstance3D.new()

var bg_mesh = BoxMesh.new(); bg_mesh.size = Vector3(1.0, 0.12, 0.04)

bg.mesh = bg_mesh

var bg_mat = StandardMaterial3D.new(); bg_mat.albedo_color = Color(0.2, 0.2, 0.2)

bg_mat.no_depth_test = true

bg.material_override = bg_mat; bg.name = "BG"

root.add_child(bg)

var bar = MeshInstance3D.new()

var bar_mesh = BoxMesh.new(); bar_mesh.size = Vector3(0.98, 0.10, 0.05)

bar.mesh = bar_mesh

var bar_mat = StandardMaterial3D.new(); bar_mat.albedo_color = Color(0.2, 0.9, 0.2)

bar_mat.no_depth_test = true; bar_mat.emission_enabled = true; bar_mat.emission = Color(0.1,0.5,0.1)

bar.material_override = bar_mat; bar.name = "Bar"

root.add_child(bar)

root.set_meta("billboard_hp", true)

return root

func _update_enemy_hp_bar(enemy: Node3D) -> void:

var hp_bar = enemy.get_meta("hp_bar") as Node3D

if hp_bar == null: return

var hp:  int = enemy.get_meta("hp")

var max_hp: int = enemy.get_meta("max_hp")

var ratio = clamp(float(hp) / float(max_hp), 0.0, 1.0)

var bar = hp_bar.get_node_or_null("Bar") as MeshInstance3D

if bar == null: return

bar.scale.x  = ratio

bar.position.x = (ratio - 1.0) * 0.5

var c: Color

if ratio > 0.5:

c = Color(1.0 - (ratio - 0.5) * 2.0, 1.0, 0.0)

else:

c = Color(1.0, ratio * 2.0, 0.0)

bar.material_override.albedo_color = c

bar.material_override.emission  = c * 0.5

var cam = get_viewport().get_camera_3d()

if cam:

hp_bar.global_rotation = cam.global_rotation

func _create_range_ring(radius: float, color: Color) -> Node3D:

var root = Node3D.new()

root.name = "RangeRing"

var steps = 48

var points = PackedVector3Array()

for i in range(steps + 1):

var a = TAU * i / steps

points.append(Vector3(cos(a) * radius, 0.05, sin(a) * radius))

var imm = ImmediateMesh.new()

imm.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)

for p in points: imm.surface_add_vertex(p)

imm.surface_end()

var mi = MeshInstance3D.new()

mi.mesh = imm

var mat = StandardMaterial3D.new()

mat.albedo_color = Color(color.r, color.g, color.b, 0.6)

mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED

mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA

mi.material_override = mat

root.add_child(mi)

return root

# ═══════════════════════════════════════════════════════════════════════════

func _build_path_visual() -> void:

for i in range(PATH_WAYPOINTS.size() - 1):

_draw_path_segment(PATH_WAYPOINTS[i], PATH_WAYPOINTS[i + 1])

func _draw_path_segment(a: Vector3, b: Vector3) -> void:

var mid = (a + b) * 0.5

var diff = b - a

var len = diff.length()

var mi = MeshInstance3D.new()

var bx = BoxMesh.new()

var is_x = abs(diff.x) > abs(diff.z)

if is_x:

bx.size = Vector3(len, 0.05, 1.8)

else:

bx.size = Vector3(1.8, 0.05, len)

mi.mesh = bx

var mat = StandardMaterial3D.new()

mat.albedo_color = Color(0.55, 0.45, 0.30)

mat.roughness  = 0.9

mi.material_override = mat

mi.global_position = Vector3(mid.x, -0.02, mid.z)

add_child(mi)

func _compute_path_cells() -> void:

path_cells.clear()

for i in range(PATH_WAYPOINTS.size() - 1):

var a = PATH_WAYPOINTS[i]

var b = PATH_WAYPOINTS[i + 1]

var dir = (b - a).normalized()

var len = a.distance_to(b)

var steps = int(len) + 2

for s in range(steps):

var p = a + dir * s

var cell = Vector3i(int(floor(p.x)), 0, int(floor(p.z)))

if not path_cells.has(cell):

path_cells.append(cell)

for dx in [-1, 0, 1]:

for dz in [-1, 0, 1]:

var nc = Vector3i(cell.x+dx, 0, cell.z+dz)

if not path_cells.has(nc):

var along_x = abs(dir.x) > abs(dir.z)

if along_x and dx == 0: path_cells.append(nc)

elif not along_x and dz == 0: path_cells.append(nc)

func _is_path_cell(cell: Vector3i) -> bool:

return path_cells.has(cell)

# ═══════════════════════════════════════════════════════════════════════════

func _mouse_to_cell() -> Vector3i:

var cam = get_viewport().get_camera_3d()

if cam == null: return Vector3i(-999, 0, -999)

var mouse = get_viewport().get_mouse_position()

var ray_from = cam.project_ray_origin(mouse)

var ray_dir = cam.project_ray_normal(mouse)

if abs(ray_dir.y) < 0.001: return Vector3i(-999, 0, -999)

var t = -ray_from.y / ray_dir.y

if t < 0: return Vector3i(-999, 0, -999)

var hit = ray_from + ray_dir * t

return Vector3i(int(floor(hit.x)), 0, int(floor(hit.z)))

# ═══════════════════════════════════════════════════════════════════════════

func _build_ui() -> void:

ui_layer = CanvasLayer.new()

ui_layer.name = "UI"

add_child(ui_layer)

var root_ctrl = Control.new()

root_ctrl.set_anchors_preset(Control.PRESET_FULL_RECT)

root_ctrl.mouse_filter = Control.MOUSE_FILTER_IGNORE

ui_layer.add_child(root_ctrl)

var top_panel = PanelContainer.new()

top_panel.set_anchors_preset(Control.PRESET_TOP_WIDE)

top_panel.custom_minimum_size = Vector2(0, 52)

root_ctrl.add_child(top_panel)

var top_hbox = HBoxContainer.new()

top_hbox.add_theme_constant_override("separation", 24)

top_panel.add_child(top_hbox)

lbl_gold = _make_label("💰 150", Color(1.0, 0.85, 0.2))

lbl_lives = _make_label("❤️ 20", Color(1.0, 0.4, 0.4))

lbl_wave = _make_label("ВОЛНА 1", Color(0.6, 1.0, 0.6))

lbl_score = _make_label("★ 0",  Color(0.9, 0.9, 0.9))

top_hbox.add_child(lbl_gold)

top_hbox.add_child(lbl_lives)

top_hbox.add_child(lbl_wave)

top_hbox.add_child(lbl_score)

speed_btn = Button.new()

speed_btn.text = "⏩ x1"

speed_btn.custom_minimum_size = Vector2(80, 0)

speed_btn.pressed.connect(_toggle_speed)

top_hbox.add_child(speed_btn)

var bot_panel = PanelContainer.new()

bot_panel.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)

bot_panel.custom_minimum_size = Vector2(0, 90)

root_ctrl.add_child(bot_panel)

var bot_vbox = VBoxContainer.new()

bot_panel.add_child(bot_vbox)

bar_container = HBoxContainer.new()

bar_container.add_theme_constant_override("separation", 8)

bar_container.alignment = BoxContainer.ALIGNMENT_CENTER

bot_vbox.add_child(bar_container)

for ttype in TOWER_DEFS.keys():

var def = TOWER_DEFS[ttype]

var btn = Button.new()

btn.custom_minimum_size = Vector2(120, 64)

btn.text = "[%s]\n%s\n💰%d" % [def["name"], def["desc"].substr(0, 20), def["cost"]]

btn.modulate = def["color"]

btn.tooltip_text = "%s\nУрон:%d Дальность:%.0f\nСкорострельность:%.1f" % [

def["desc"], def["damage"], def["range"], def["fire_rate"]]

btn.pressed.connect(_select_tower.bind(ttype))

bar_container.add_child(btn)

tower_buttons[ttype] = btn

var hint = Label.new()

hint.text = "ЛКМ — поставить башню | ПКМ — продать башню"

hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER

hint.modulate = Color(0.7, 0.7, 0.7)

bot_vbox.add_child(hint)

btn_start = Button.new()

btn_start.text = "▶ ВОЛНА 1 / %d" % WAVES.size()

btn_start.custom_minimum_size = Vector2(260, 54)

btn_start.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)

btn_start.position.y = -140

btn_start.pressed.connect(_start_wave)

root_ctrl.add_child(btn_start)

lbl_msg = Label.new()

lbl_msg.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER

lbl_msg.vertical_alignment = VERTICAL_ALIGNMENT_CENTER

lbl_msg.set_anchors_preset(Control.PRESET_CENTER)

lbl_msg.custom_minimum_size = Vector2(420, 160)

lbl_msg.position      = Vector2(-210, -80)

lbl_msg.add_theme_font_size_override("font_size", 22)

lbl_msg.modulate = Color(1, 1, 0.5)

lbl_msg.visible = false

root_ctrl.add_child(lbl_msg)

_select_tower("basic")

func _make_label(text: String, color: Color) -> Label:

var l = Label.new()

l.text = text

l.modulate = color

l.add_theme_font_size_override("font_size", 18)

l.custom_minimum_size = Vector2(120, 0)

return l

func _select_tower(ttype: String) -> void:

selected_tower_type = ttype

for t in tower_buttons.keys():

tower_buttons[t].modulate = TOWER_DEFS[t]["color"] if t != ttype else Color.WHITE

if preview_node:

var rng_ring = preview_node.get_node_or_null("PreviewRange")

if rng_ring: rng_ring.queue_free()

var def = TOWER_DEFS[ttype]

var new_ring = _create_range_ring(def["range"], def["color"])

new_ring.name = "PreviewRange"

new_ring.set_meta("for_type", ttype)

preview_node.add_child(new_ring)

func _toggle_speed() -> void:

if game_speed == 1.0:

game_speed = 2.0; speed_btn.text = "⏩ x2"

elif game_speed == 2.0:

game_speed = 3.0; speed_btn.text = "⏩ x3"

else:

game_speed = 1.0; speed_btn.text = "⏩ x1"

func _update_ui() -> void:

if lbl_gold: lbl_gold.text = "💰 %d" % gold

if lbl_lives: lbl_lives.text = "❤️ %d" % lives

if lbl_wave: lbl_wave.text = "ВОЛНА %d/%d" % [wave_index + 1, WAVES.size()]

if lbl_score: lbl_score.text = "★ %d" % score

if bar_container:

for ttype in tower_buttons.keys():

tower_buttons[ttype].disabled = gold < TOWER_DEFS[ttype]["cost"]

func _show_message(text: String, auto_hide: float = 0.0) -> void:

if lbl_msg == null: return

lbl_msg.text  = text

lbl_msg.visible = true

if auto_hide > 0.0:

await get_tree().create_timer(auto_hide).timeout

lbl_msg.visible = false

# ═══════════════════════════════════════════════════════════════════════════

func _game_over() -> void:

game_state = "gameover"

btn_start.visible = false

_show_message("💀 ПОРАЖЕНИЕ\nОчки: %d\n\nПерезапусти сцену (F5)" % score)

func _victory() -> void:

game_state = "victory"

btn_start.visible = false

_show_message("🏆 ПОБЕДА!\nВсе волны отбиты!\nОчки: %d\n\nПерезапусти (F5)" % score)