Загрузка данных


extends Node2D

const W := 2304.0
const H := 1296.0
const WALL := 24.0
const WALL_COLOR := Color(0.5, 0.5, 0.5)
const ZONE_W := 400.0
const CELL := 24.0          # размер клетки сетки для поиска пути
const NAV_MARGIN := 18.0    # отступ от стен (радиус юнита + запас)

# зум колесом мыши
const ZOOM_MIN := 0.1       # можно отдалить так, что карта станет совсем маленькой
const ZOOM_MAX := 3.0
const ZOOM_STEP := 1.15

var wall_rects: Array[Rect2] = []
var astar: AStarGrid2D
var cols := 0
var rows := 0
var menu: SpawnMenu
var preview: Node2D
var markers: Node2D
var cam: Camera2D = null
var zoom_target := 1.0
var block_fire := false     # игрок не стреляет, пока выбран серый юнит или курсор над меню
var count_timer := 0.0

func _ready() -> void:
	RenderingServer.set_default_clear_color(Color(0.04, 0.04, 0.06))
	_build_floor()
	_build_walls()
	_build_nav()
	preview = Node2D.new()
	preview.z_index = 50
	preview.draw.connect(_draw_preview)
	add_child(preview)
	menu = SpawnMenu.new()
	add_child(menu)
	menu.clear_requested.connect(_clear_units)
	menu.spawn_requested.connect(_on_spawn_requested)
	_spawn_player()

func _process(delta: float) -> void:
	var over_ui := menu.is_mouse_over(get_viewport().get_mouse_position())
	block_fire = menu.selected != null or over_ui
	preview.visible = menu.selected != null and not over_ui
	preview.position = get_global_mouse_position()
	preview.queue_redraw()

	# плавный зум
	if cam != null and is_instance_valid(cam):
		cam.zoom = cam.zoom.lerp(Vector2.ONE * zoom_target, clampf(delta * 12.0, 0.0, 1.0))

	count_timer -= delta
	if count_timer <= 0.0:
		count_timer = 0.25
		var c := {Unit.Team.BLUE: 0, Unit.Team.RED: 0, Unit.Team.NONE: 0}
		for n in get_tree().get_nodes_in_group("units"):
			var u := n as Unit
			if u != null and not u.dead:
				c[u.team] += 1
		menu.set_counts(c[Unit.Team.BLUE], c[Unit.Team.RED], c[Unit.Team.NONE])

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.pressed:
		match event.button_index:
			MOUSE_BUTTON_WHEEL_UP:
				zoom_target = clampf(zoom_target * ZOOM_STEP, ZOOM_MIN, ZOOM_MAX)
			MOUSE_BUTTON_WHEEL_DOWN:
				zoom_target = clampf(zoom_target / ZOOM_STEP, ZOOM_MIN, ZOOM_MAX)
			MOUSE_BUTTON_LEFT:
				if menu.selected != null:
					var pos := get_global_mouse_position()
					if is_free(pos):
						spawn_unit(menu.selected, menu.team, pos)
			MOUSE_BUTTON_RIGHT:
				menu.clear_selection()
	elif event is InputEventKey and event.pressed and not event.echo:
		match event.physical_keycode:
			KEY_E:
				var pos := get_global_mouse_position()
				if is_free(pos):
					spawn_unit({"ai": false, "gun": false}, Unit.Team.NONE, pos)
			KEY_Q: menu.toggle()
			KEY_ESCAPE: menu.clear_selection()
			KEY_1: menu.set_team(Unit.Team.BLUE)
			KEY_2: menu.set_team(Unit.Team.RED)
			KEY_3: menu.set_team(Unit.Team.NONE)

func _on_spawn_requested(entry: Dictionary, team: int) -> void:
	spawn_unit(entry, team, spawn_point(team))

func spawn_unit(entry: Dictionary, team: int, pos: Vector2) -> void:
	var u := Unit.new()
	u.team = team
	u.ai_enabled = entry["ai"]
	u.has_gun = entry["gun"]
	u.position = pos
	add_child(u)

func _clear_units() -> void:
	for n in get_tree().get_nodes_in_group("units"):
		var u := n as Unit
		if u != null and not u.is_player:
			u.queue_free()

func _draw_preview() -> void:
	if menu.selected == null:
		return
	var col := Unit.team_color(menu.team)
	col.a = 0.6 if is_free(preview.position) else 0.15
	preview.draw_circle(Vector2.ZERO, Unit.RADIUS, col)
	preview.draw_arc(Vector2.ZERO, Unit.RADIUS, 0, TAU, 32, Color(1, 1, 1, 0.5), 2.0)

# ---------------- точки спавна ----------------
func spawn_points(team: int) -> Array:
	# 3 точки на базе: сверху, по центру, снизу
	var x := WALL + 120.0
	if team == Unit.Team.RED:
		x = W - WALL - 120.0
	return [Vector2(x, H * 0.25), Vector2(x, H * 0.5), Vector2(x, H * 0.75)]

func spawn_point(team: int) -> Vector2:
	var pts := spawn_points(team)
	var p: Vector2 = pts[randi() % pts.size()]
	return p + Vector2(randf_range(-25, 25), randf_range(-25, 25))   # небольшой разброс, чтобы не слипались

func _draw_markers() -> void:
	for t in [Unit.Team.BLUE, Unit.Team.RED]:
		var col := Unit.team_color(t)
		for p in spawn_points(t):
			markers.draw_circle(p, 34.0, Color(col.r, col.g, col.b, 0.12))
			markers.draw_arc(p, 34.0, 0, TAU, 40, Color(col.r, col.g, col.b, 0.5), 2.0)

# ---------------- карта ----------------
func _build_floor() -> void:
	_add_rect(Rect2(0, 0, W, H), Color(0.12, 0.12, 0.14), -10)
	_add_rect(base_rect(Unit.Team.BLUE), Color(0.2, 0.5, 1.0, 0.15), -9)
	_add_rect(base_rect(Unit.Team.RED), Color(1.0, 0.25, 0.25, 0.15), -9)
	markers = Node2D.new()
	markers.z_index = -8
	markers.draw.connect(_draw_markers)
	add_child(markers)

func _build_walls() -> void:
	_add_wall(Rect2(0, 0, W, WALL))
	_add_wall(Rect2(0, H - WALL, W, WALL))
	_add_wall(Rect2(0, 0, WALL, H))
	_add_wall(Rect2(W - WALL, 0, WALL, H))
	_add_wall(Rect2(W / 2 - 20, 150, 40, 300))
	_add_wall(Rect2(W / 2 - 20, H - 450, 40, 300))
	_add_wall(Rect2(W / 2 - 200, H / 2 - 20, 400, 40))
	_add_wall(Rect2(W / 2 - 600, 300, 160, 40))
	_add_wall(Rect2(W / 2 + 440, 300, 160, 40))
	_add_wall(Rect2(W / 2 - 600, H - 340, 160, 40))
	_add_wall(Rect2(W / 2 + 440, H - 340, 160, 40))
	_add_wall(Rect2(W / 2 - 800, H / 2 - 100, 40, 200))
	_add_wall(Rect2(W / 2 + 760, H / 2 - 100, 40, 200))
	_add_wall(Rect2(520, 250, 24, 250))
	_add_wall(Rect2(520, H - 500, 24, 250))
	_add_wall(Rect2(W - 544, 250, 24, 250))
	_add_wall(Rect2(W - 544, H - 500, 24, 250))

func _add_wall(r: Rect2) -> void:
	wall_rects.append(r)
	var body := StaticBody2D.new()
	body.collision_layer = 2   # стены на слое 2
	body.collision_mask = 0
	body.position = r.position + r.size / 2
	var shape := CollisionShape2D.new()
	var rect := RectangleShape2D.new()
	rect.size = r.size
	shape.shape = rect
	body.add_child(shape)
	var vis := ColorRect.new()
	vis.size = r.size
	vis.position = -r.size / 2
	vis.color = WALL_COLOR
	body.add_child(vis)
	add_child(body)

func _add_rect(r: Rect2, c: Color, z: int) -> void:
	var vis := ColorRect.new()
	vis.position = r.position
	vis.size = r.size
	vis.color = c
	vis.z_index = z
	add_child(vis)

# ---------------- навигация для ИИ ----------------
func _build_nav() -> void:
	cols = int(W / CELL)
	rows = int(H / CELL)
	astar = AStarGrid2D.new()
	astar.size = Vector2i(cols, rows)
	astar.cell_size = Vector2(CELL, CELL)
	astar.offset = Vector2(CELL / 2, CELL / 2)
	astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES
	astar.update()
	for x in cols:
		for y in rows:
			var c := Vector2(x * CELL + CELL / 2, y * CELL + CELL / 2)
			for r in wall_rects:
				if r.grow(NAV_MARGIN).has_point(c):
					astar.set_point_solid(Vector2i(x, y), true)
					break

func _cell_of(pos: Vector2) -> Vector2i:
	return Vector2i(clampi(int(pos.x / CELL), 0, cols - 1), clampi(int(pos.y / CELL), 0, rows - 1))

func _nearest_free_cell(pos: Vector2) -> Vector2i:
	var c := _cell_of(pos)
	if not astar.is_point_solid(c):
		return c
	for r in range(1, 8):
		for dx in range(-r, r + 1):
			for dy in range(-r, r + 1):
				if maxi(absi(dx), absi(dy)) != r:
					continue
				var n := Vector2i(c.x + dx, c.y + dy)
				if n.x >= 0 and n.x < cols and n.y >= 0 and n.y < rows and not astar.is_point_solid(n):
					return n
	return c

func find_path(from: Vector2, to: Vector2) -> PackedVector2Array:
	return astar.get_point_path(_nearest_free_cell(from), _nearest_free_cell(to))

func is_free(pos: Vector2) -> bool:
	if pos.x < 0 or pos.y < 0 or pos.x > W or pos.y > H:
		return false
	return not astar.is_point_solid(_cell_of(pos))

func base_rect(team: int) -> Rect2:
	if team == Unit.Team.RED:
		return Rect2(W - WALL - ZONE_W, WALL, ZONE_W, H - WALL * 2)
	return Rect2(WALL, WALL, ZONE_W, H - WALL * 2)

func random_free_point(rect: Rect2) -> Vector2:
	for i in 30:
		var p := Vector2(randf_range(rect.position.x, rect.end.x), randf_range(rect.position.y, rect.end.y))
		if is_free(p):
			return p
	return rect.get_center()

func base_point(team: int) -> Vector2:
	return random_free_point(base_rect(team))

func patrol_point(team: int) -> Vector2:
	var rect := Rect2(WALL, WALL, W - WALL * 2, H - WALL * 2)
	if randf() < 0.75:   # чаще идёт на половину врага
		if team == Unit.Team.BLUE:
			rect = Rect2(W * 0.5, WALL, W * 0.5 - WALL, H - WALL * 2)
		else:
			rect = Rect2(WALL, WALL, W * 0.5 - WALL, H - WALL * 2)
	return random_free_point(rect)

# ---------------- игрок ----------------
func _spawn_player() -> void:
	var p := Unit.new()
	p.team = Unit.Team.BLUE
	p.is_player = true
	p.position = spawn_point(Unit.Team.BLUE)
	p.died.connect(_on_player_died)
	add_child(p)

	# камера без границ, чтобы можно было сильно отдалять
	cam = Camera2D.new()
	cam.zoom = Vector2.ONE * zoom_target
	cam.position_smoothing_enabled = true
	cam.position_smoothing_speed = 10.0
	p.add_child(cam)

func _on_player_died(_u) -> void:
	await get_tree().create_timer(2.0).timeout
	_spawn_player()