Загрузка данных
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()