Участник:Prionosuchus

Материал из Абсурдопедии
Перейти к навигацииПерейти к поиску

Prionosuchus — древняя амфибия, жившая в пермском периоде (т.е. до Хренозоя).

Коды для игр[править]

Абсурдосубнатика[править]

Превращаю из обычной в абсурдную постепенно.

1.1-е обновление

  • исправлено: карта почти нужного вида
  • добавлено: ЧБОС-кит

1.2-е обновление

  • добавлены рыбы-кабачки и рыбы-баклажаны
  • а также делящийся при повреждении бешеный мох

2-е обновление:

  • добавлены големы, рыбы шурупы, отвёртки, гвозди, болты.
  • добавлен ядерный гриб
  • убрана вся фауна оригинала

3-е обновление:

  • добавлена птица-Говносрун и её еда — горох
  • добавлен Акласур
  • добавлена пиявка-гидра
  • проведено деление авроры на отсеки и оптимизация
  • скрафтить можно в любых обломках

4-е обновление:

  • добавлен джип и бронетранспортёр
  • можно пробивать стены торпедами
  • добавлено 100 рабочих, а игрок теперь - их прораб (можно отключить в настройках N)
  • добавлены патроны (теперь нельзя стрелять бесконечно)
  • добавлены новые рыбы:
    • Рыба-огурец - 0,8 м Быстрая, зелёная, покрыта мелкими шипиками
    • Рыба-патиссон - 1,5 м Медленная, плоская, прочность втрое больше рыбы-кабачка
    • Рыба-гайка - 0,3 м При опасности прикрепляется к рыбе-болту
    • Рыба-болт - Носитель рыб-гаек
    • Гидрорыба - Хищник с короткими ротовыми щупальцами
  • говносрун теперь стреляет мощной очередью, а его снаряды разрушаются при ударе
  • исправлено название АКЛАСУРа
  • теперь есть ресурсы и в самой Авроре

Сам код ниже:

# -*- coding: utf-8 -*-
# МегоСубнатика2D — полная обновлённая игра.
# Настройка начальных предметов: START_INVENTORY.
# Установка зависимости: pip install pygame
# THREE_SPECIES_REMOVED_V1
import pygame
import random
import math
import sys
import os
from collections import OrderedDict
AA = 2

def P(x, y):
    return (int(x * AA), int(y * AA))

def poly(surface, color, points, width=0):
    pts = [P(x, y) for x, y in points]
    pygame.draw.polygon(surface, color, pts, width * AA)

def ellipse(surface, color, rect, width=0):
    x, y, w, h = rect
    pygame.draw.ellipse(surface, color, pygame.Rect(int(x * AA), int(y * AA), int(w * AA), int(h * AA)), width * AA)

def rect(surface, color, r, width=0, radius=0):
    x, y, w, h = r
    pygame.draw.rect(surface, color, pygame.Rect(int(x * AA), int(y * AA), int(w * AA), int(h * AA)), width * AA, border_radius=radius * AA)

def line(surface, color, a, b, width=1):
    pygame.draw.line(surface, color, P(*a), P(*b), width * AA)

def circle(surface, color, pos, radius, width=0):
    pygame.draw.circle(surface, color, P(*pos), int(radius * AA), width * AA)

def aaline(surface, color, points, closed=False):
    pygame.draw.aalines(surface, color, closed, [P(x, y) for x, y in points])

def draw_cyclops(surface, cx, cy, scale=1.0):

    def pt(x, y):
        return (cx + x * scale, cy + y * scale)
    shadow = [pt(-295, -45), pt(-260, -72), pt(-150, -91), pt(70, -88), pt(205, -65), pt(290, -33), pt(318, 0), pt(290, 33), pt(205, 65), pt(70, 88), pt(-150, 91), pt(-260, 72), pt(-295, 45)]
    poly(surface, (0, 7, 10), shadow)
    body = [pt(-300, 0), pt(-283, -40), pt(-245, -67), pt(-180, -82), pt(-60, -87), pt(85, -81), pt(183, -68), pt(248, -46), pt(289, -21), pt(307, 0), pt(289, 21), pt(248, 46), pt(183, 68), pt(85, 81), pt(-60, 87), pt(-180, 82), pt(-245, 67), pt(-283, 40)]
    poly(surface, (126, 132, 126), body)
    pygame.draw.aalines(surface, (33, 38, 37), True, [P(x, y) for x, y in body])
    center_panel = [pt(-225, -54), pt(-135, -66), pt(65, -67), pt(175, -55), pt(248, -32), pt(267, 0), pt(248, 32), pt(175, 55), pt(65, 67), pt(-135, 66), pt(-225, 54), pt(-254, 0)]
    poly(surface, (177, 181, 172), center_panel)
    orange = (224, 102, 28)
    orange_dark = (151, 61, 18)
    orange_light = (246, 131, 39)
    top_orange = [pt(-205, -61), pt(-110, -76), pt(70, -74), pt(161, -61), pt(174, -46), pt(58, -50), pt(-116, -51)]
    bottom_orange = [pt(-205, 61), pt(-110, 76), pt(70, 74), pt(161, 61), pt(174, 46), pt(58, 50), pt(-116, 51)]
    poly(surface, orange_dark, top_orange)
    poly(surface, orange_dark, bottom_orange)
    inner_top = [pt(-160, -58), pt(-80, -68), pt(60, -67), pt(137, -57), pt(146, -50), pt(54, -54), pt(-92, -54)]
    inner_bottom = [pt(-160, 58), pt(-80, 68), pt(60, 67), pt(137, 57), pt(146, 50), pt(54, 54), pt(-92, 54)]
    poly(surface, orange, inner_top)
    poly(surface, orange, inner_bottom)
    line(surface, orange_light, pt(-120, -61), pt(75, -61), 2)
    line(surface, orange_light, pt(-120, 61), pt(75, 61), 2)
    spine = [pt(-245, -20), pt(-130, -30), pt(85, -28), pt(210, -18), pt(269, 0), pt(210, 18), pt(85, 28), pt(-130, 30), pt(-245, 20)]
    poly(surface, (77, 82, 79), spine)
    spine2 = [pt(-215, -12), pt(-105, -18), pt(91, -16), pt(192, -10), pt(237, 0), pt(192, 10), pt(91, 16), pt(-105, 18), pt(-215, 12)]
    poly(surface, (112, 117, 111), spine2)
    tower = [pt(-48, -32), pt(59, -30), pt(102, -20), pt(115, 0), pt(102, 20), pt(59, 30), pt(-48, 32), pt(-78, 19), pt(-84, 0), pt(-78, -19)]
    poly(surface, (48, 53, 52), tower)
    tower_inner = [pt(-38, -22), pt(50, -21), pt(82, -14), pt(94, 0), pt(82, 14), pt(50, 21), pt(-38, 22), pt(-63, 13), pt(-68, 0), pt(-63, -13)]
    poly(surface, (93, 102, 99), tower_inner)
    glass = (20, 76, 88)
    glass_light = (49, 139, 152)
    windows = [(-24, -15, 25, 12), (5, -15, 25, 12), (34, -14, 23, 11), (61, -11, 17, 9), (-24, 3, 25, 12), (5, 3, 25, 12), (34, 3, 23, 11), (61, 2, 17, 9)]
    for x, y, w, h in windows:
        rect(surface, glass, (cx + x * scale, cy + y * scale, w * scale, h * scale), radius=max(1, int(3 * scale)))
        line(surface, glass_light, pt(x + 3, y + 3), pt(x + w - 3, y + 3), max(1, int(1.5 * scale)))
    ellipse(surface, (43, 47, 46), (cx - 122 * scale, cy - 17 * scale, 39 * scale, 34 * scale))
    ellipse(surface, (122, 130, 125), (cx - 116 * scale, cy - 12 * scale, 27 * scale, 24 * scale), width=max(1, int(3 * scale)))
    line(surface, (35, 38, 38), pt(-115, 0), pt(-91, 0), max(1, int(2 * scale)))
    for x in [-190, -145, -100, 120, 155, 190]:
        line(surface, (69, 73, 70), pt(x, -48), pt(x + 8, 48), max(1, int(scale)))
    for x in [-190, -142, -94, 118, 156]:
        rect(surface, (67, 71, 68), (cx + x * scale, cy - 15 * scale, 32 * scale, 30 * scale), width=max(1, int(scale)), radius=max(1, int(3 * scale)))
    stern = [pt(-300, -36), pt(-336, -30), pt(-356, -17), pt(-365, 0), pt(-356, 17), pt(-336, 30), pt(-300, 36), pt(-282, 20), pt(-282, -20)]
    poly(surface, (58, 61, 60), stern)
    engine_positions = [-46, 46]
    for ey in engine_positions:
        poly(surface, (62, 65, 62), [pt(-238, ey - 20), pt(-319, ey - 25), pt(-335, ey - 14), pt(-335, ey + 14), pt(-319, ey + 25), pt(-238, ey + 20)])
        ellipse(surface, (39, 42, 42), (cx - 355 * scale, cy + (ey - 20) * scale, 72 * scale, 40 * scale))
        ellipse(surface, (103, 109, 103), (cx - 344 * scale, cy + (ey - 14) * scale, 46 * scale, 28 * scale))
        ellipse(surface, (18, 31, 31), (cx - 337 * scale, cy + (ey - 10) * scale, 31 * scale, 20 * scale))
        ellipse(surface, (38, 157, 175), (cx - 334 * scale, cy + (ey - 7) * scale, 20 * scale, 14 * scale))
        ellipse(surface, (122, 226, 231), (cx - 330 * scale, cy + (ey - 4) * scale, 11 * scale, 8 * scale))
    poly(surface, (72, 77, 74), [pt(-178, -71), pt(-130, -118), pt(-50, -113), pt(-72, -76)])
    poly(surface, (72, 77, 74), [pt(-178, 71), pt(-130, 118), pt(-50, 113), pt(-72, 76)])
    poly(surface, orange, [pt(-131, -108), pt(-78, -103), pt(-95, -79), pt(-153, -82)])
    poly(surface, orange, [pt(-131, 108), pt(-78, 103), pt(-95, 79), pt(-153, 82)])
    nose = [pt(213, -40), pt(273, -28), pt(306, -8), pt(312, 0), pt(306, 8), pt(273, 28), pt(213, 40), pt(235, 18), pt(246, 0), pt(235, -18)]
    poly(surface, (198, 200, 188), nose)
    for yy in [-16, 4]:
        rect(surface, (19, 79, 89), (cx + 244 * scale, cy + yy * scale, 34 * scale, 12 * scale), radius=max(1, int(3 * scale)))
    for yy in [-25, 25]:
        circle(surface, (55, 64, 61), pt(261, yy), 7 * scale)
        circle(surface, (186, 229, 217), pt(263, yy), 4 * scale)
        circle(surface, (235, 255, 236), pt(264, yy), 2 * scale)
    line(surface, (215, 112, 37), pt(112, -35), pt(190, -29), max(1, int(4 * scale)))
    line(surface, (215, 112, 37), pt(112, 35), pt(190, 29), max(1, int(4 * scale)))

def draw_seamoth(surface, cx, cy, scale=1.0):

    def pt(x, y):
        return (cx + x * scale, cy + y * scale)
    white = (210, 213, 201)
    light = (234, 235, 217)
    dark = (54, 59, 57)
    orange = (225, 111, 27)
    orange2 = (247, 142, 39)
    glass = (17, 72, 84)
    glass_light = (67, 169, 179)
    ellipse(surface, (0, 7, 10), (cx - 105 * scale, cy - 58 * scale, 215 * scale, 116 * scale))
    poly(surface, dark, [pt(-20, -34), pt(-79, -89), pt(-122, -83), pt(-93, -43), pt(-61, -19)])
    poly(surface, dark, [pt(-20, 34), pt(-79, 89), pt(-122, 83), pt(-93, 43), pt(-61, 19)])
    poly(surface, white, [pt(-42, -40), pt(-81, -76), pt(-106, -71), pt(-82, -44), pt(-55, -25)])
    poly(surface, white, [pt(-42, 40), pt(-81, 76), pt(-106, 71), pt(-82, 44), pt(-55, 25)])
    for yy in (-72, 72):
        ellipse(surface, (40, 45, 44), (cx - 113 * scale, cy + (yy - 16) * scale, 45 * scale, 32 * scale))
        ellipse(surface, (105, 111, 106), (cx - 105 * scale, cy + (yy - 11) * scale, 29 * scale, 22 * scale))
        ellipse(surface, (20, 110, 126), (cx - 98 * scale, cy + (yy - 7) * scale, 17 * scale, 14 * scale))
        ellipse(surface, (90, 220, 225), (cx - 93 * scale, cy + (yy - 4) * scale, 8 * scale, 8 * scale))
    body = [pt(-86, 0), pt(-71, -32), pt(-40, -50), pt(0, -58), pt(42, -51), pt(78, -33), pt(103, -10), pt(109, 0), pt(103, 10), pt(78, 33), pt(42, 51), pt(0, 58), pt(-40, 50), pt(-71, 32)]
    poly(surface, white, body)
    outline = [pt(-79, 0), pt(-62, -27), pt(-32, -43), pt(5, -48), pt(42, -42), pt(72, -27), pt(94, -8), pt(98, 0), pt(94, 8), pt(72, 27), pt(42, 42), pt(5, 48), pt(-32, 43), pt(-62, 27)]
    pygame.draw.aalines(surface, (57, 62, 59), True, [P(x, y) for x, y in outline])
    poly(surface, orange, [pt(-48, -42), pt(-24, -52), pt(-9, -54), pt(-19, -29), pt(-39, -23)])
    poly(surface, orange, [pt(-48, 42), pt(-24, 52), pt(-9, 54), pt(-19, 29), pt(-39, 23)])
    cockpit = [pt(-23, -33), pt(21, -37), pt(55, -27), pt(77, -11), pt(84, 0), pt(77, 11), pt(55, 27), pt(21, 37), pt(-23, 33), pt(-42, 20), pt(-48, 0), pt(-42, -20)]
    poly(surface, dark, cockpit)
    glass_shape = [pt(-13, -27), pt(20, -30), pt(49, -22), pt(68, -9), pt(74, 0), pt(68, 9), pt(49, 22), pt(20, 30), pt(-13, 27), pt(-30, 16), pt(-35, 0), pt(-30, -16)]
    poly(surface, glass, glass_shape)
    line(surface, glass_light, pt(-17, -20), pt(42, -17), max(1, int(2 * scale)))
    line(surface, (119, 205, 208), pt(41, -15), pt(60, -7), max(1, int(2 * scale)))
    line(surface, (37, 44, 43), pt(18, -29), pt(18, 29), max(1, int(3 * scale)))
    poly(surface, light, [pt(70, -28), pt(101, -12), pt(112, 0), pt(101, 12), pt(70, 28), pt(83, 10), pt(87, 0), pt(83, -10)])
    for yy in [-15, 15]:
        circle(surface, (47, 56, 54), pt(92, yy), 7 * scale)
        circle(surface, (181, 224, 214), pt(94, yy), 4.5 * scale)
        circle(surface, (244, 255, 235), pt(95, yy), 2 * scale)
    poly(surface, (72, 77, 73), [pt(-86, -22), pt(-111, -18), pt(-123, -8), pt(-127, 0), pt(-123, 8), pt(-111, 18), pt(-86, 22)])
    ellipse(surface, (39, 44, 43), (cx - 130 * scale, cy - 18 * scale, 37 * scale, 36 * scale))
    ellipse(surface, (22, 124, 140), (cx - 125 * scale, cy - 12 * scale, 24 * scale, 24 * scale))
    ellipse(surface, (94, 224, 227), (cx - 120 * scale, cy - 8 * scale, 15 * scale, 16 * scale))
    ellipse(surface, (75, 82, 78), (cx - 72 * scale, cy - 18 * scale, 35 * scale, 36 * scale))
    ellipse(surface, orange2, (cx - 66 * scale, cy - 12 * scale, 23 * scale, 24 * scale), width=max(1, int(3 * scale)))
    for bx, by in [(-55, -33), (-55, 33), (62, -28), (62, 28)]:
        circle(surface, (79, 82, 78), pt(bx, by), 3 * scale)

def _make_reference_vehicle_sprite(draw_func, logical_size, center, target_width):
    logical_w, logical_h = logical_size
    source = pygame.Surface((logical_w * AA, logical_h * AA), pygame.SRCALPHA)
    draw_func(source, center[0], center[1], 1.0)
    bounds = source.get_bounding_rect(min_alpha=1)
    if bounds.width > 0 and bounds.height > 0:
        source = source.subsurface(bounds).copy()
    target_width = max(1, int(target_width))
    target_height = max(1, int(round(target_width * source.get_height() / max(1, source.get_width()))))
    return pygame.transform.smoothscale(source, (target_width, target_height)).convert_alpha()

def generate_detailed_cyclops_vehicle_sprite(target_width=180):
    return _make_reference_vehicle_sprite(draw_cyclops, (760, 270), (390, 135), target_width)

def generate_detailed_seamoth_vehicle_sprite(target_width=86):
    return _make_reference_vehicle_sprite(draw_seamoth, (300, 220), (150, 110), target_width)
MAP_SIZE = 1000
WHALE_NAME = 'Чёрно-бело-оранжево-синий кит'
WHALE_HP = 10000
WHALE_SPEED = 2.0
WHALE_PUNISHMENT_DRAGONS = 20
LAND_BIOMES = frozenset(('Горы', 'Побережье', 'Острова', 'Леса'))
WALK_SPEED = 0.94
TREE_SPECIES = ('Сосна', 'Ель', 'Дуб', 'Берёза', 'Краснолистное дерево', 'Пальма')
FOREST_TREES_PER_400_CELLS = 25
MAD_MOSS_MIN_DIAMETER = 0.2
MAD_MOSS_MAX_DIAMETER = 1.5
MAD_MOSS_SPEED = 0.1
MAD_MOSS_GROWTH_PER_FRAME = 0.0005
MAD_MOSS_PER_400_FOREST = 1.6
MAD_MOSS_MAX_COLONIES = 650
GOLEM_ENABLED = True
GOLEM_MAX_COLONIES = 240
GOLEM_WIND_SPEED = 0.018
GOLEM_FORMATION_MASS = 100.0
GOLEM_REASSEMBLE_MIN = 15 * 60
GOLEM_REASSEMBLE_MAX = 32 * 60
GOLEM_DAMAGE_COOLDOWN = 75
GOLEM_DATA = {'stone': {'name': 'Каменный голем', 'hp': 50, 'damage': 10, 'diameter': 2.0, 'speed': 0.025, 'color': (120, 132, 140)}, 'water': {'name': 'Водяной голем', 'hp': 32, 'damage': 5, 'diameter': 1.65, 'speed': 0.055, 'color': (58, 158, 226)}, 'wood': {'name': 'Деревянный голем', 'hp': 38, 'damage': 7, 'diameter': 1.85, 'speed': 0.035, 'color': (126, 91, 53)}, 'dung': {'name': 'Говняный голем', 'hp': 19, 'damage': 3, 'diameter': 1.4, 'speed': 0.065, 'color': (115, 79, 42)}}

def is_land_biome(biome):
    return biome is not None and biome.name in LAND_BIOMES
CELL_SIZE = 30
VIEW_RADIUS = 10
CAMERA_ZOOM_MIN = 0.6
CAMERA_ZOOM_MAX = 1.8
CAMERA_ZOOM_STEP = 1.15
ZOOM_DETAIL_ENABLED = True
ZOOM_DETAIL_START = 1.1
ZOOM_DETAIL_SOURCE_FACTOR = 2.6
ZOOM_DETAIL_ROTATION_STEP = 6
ZOOM_DETAIL_ROTATED_CACHE_MAX = 90
NIGHT_EDGE_VIGNETTE = True
NIGHT_EDGE_FADE = 0.43
NIGHT_PLANKTON_ENABLED = True
PREDATOR_DETECTION_RANGE_BY_NAME = {'Морской дракон-левиафан': 6.5, 'Чёрно-бело-оранжево-синий кит': 8.0}
PREDATOR_DETECTION_RANGE_DEFAULT = 4.0
NIGHT_PLANKTON_MAX = 520
NIGHT_FRY_CHANCE = 0.18
NIGHT_FLEE_CHANCE = 0.36
NIGHT_VISIBLE_BRIGHTNESS = 0.5
SCREEN_SIZE = (VIEW_RADIUS * 2 + 1) * CELL_SIZE
INVENTORY_SIZE = 30
INVENTORY_COLS = 6
INVENTORY_ROWS = 5
START_INVENTORY = {'Мотылёк': 1, 'Циклоп': 1, 'Лёгкий джип': 1, 'Бронетранспортёр': 1, 'Ремкомплект': 3, 'Сканер': 1, 'Нож': 1, 'Подводное ружьё': 1, 'Коробка патронов (20)': 1, 'Морской дракон-левиафан': 1}
START_ITEM_REGISTRY = {}
LIGHT_ATTRACTION = {}
LIGHT_AVERSION = {'Рыба-баклажан': 1.0}
LIGHTING_ENABLED = True
VEHICLE_HEADLIGHTS_DEFAULT = True
FLARES_DO_NOT_AFFECT_PREDATORS_AT_NIGHT = True
SLOT_SIZE = 50
SLOT_MARGIN = 10
WORLD_CHUNK_SIZE = 32
CAVE_CHUNK_SIZE = 32
ANIMAL_CHUNK_SIZE = 16
ROTATION_STEP = 3
MAX_BIOME_CACHE = 50000
MAX_CAVE_CACHE = 50000
FISH_COUNT_MULTIPLIER = 3
CAVE_TUNNEL_ECOSYSTEM_OVERRIDES = {}
BIOLUMINESCENCE_ENABLED = True
MIDNIGHT_BLACKOUT = True
MIDNIGHT_BLACKOUT_HALF_WINDOW = 0.05
BIOGLOW_DAY_ALPHA = 105
BIOGLOW_NIGHT_ALPHA = 250
START_CAPSULE_WIDTH = 8
START_CAPSULE_HEIGHT = 6
START_CAPSULE_INNER_WIDTH = 6
START_CAPSULE_INNER_HEIGHT = 4

def quantize_angle(angle):
    return int(round(angle / ROTATION_STEP) * ROTATION_STEP) % 360

def load_image_with_proportions(path, target_size, colorkey=(255, 255, 255)):
    if not os.path.exists(path):
        return None
    image = pygame.image.load(path).convert()
    image.set_colorkey(colorkey)
    original_width, original_height = image.get_size()
    scale = min(target_size / original_width, target_size / original_height)
    new_width = max(1, int(original_width * scale))
    new_height = max(1, int(original_height * scale))
    scaled = pygame.transform.smoothscale(image, (new_width, new_height))
    result = pygame.Surface((target_size, target_size), pygame.SRCALPHA)
    x = (target_size - new_width) // 2
    y = (target_size - new_height) // 2
    result.blit(scaled, (x, y))
    return result.convert_alpha()

def generate_pike_style_sprite(length, width, body_color, spine_color, belly_color, fin_color, spot_colors, tail_color=(255, 165, 0), eye_color=(255, 255, 255), pupil_color=(0, 0, 0)):
    w = max(8, int(length))
    h = max(4, int(width))
    surf = pygame.Surface((w, h), pygame.SRCALPHA)
    pygame.draw.ellipse(surf, body_color, (0, 0, w, h))
    pygame.draw.ellipse(surf, spine_color, pygame.Rect(0, 0, w, h // 2 + 2))
    pygame.draw.ellipse(surf, belly_color, pygame.Rect(0, h // 2 - 2, w, h // 2 + 2))
    eye_radius = max(2, int(0.09 * h))
    pupil_radius = max(1, int(0.05 * h))
    eye_x = int(0.8 * w)
    eye_y1 = int(0.35 * h)
    eye_y2 = int(0.65 * h)
    pygame.draw.circle(surf, eye_color, (eye_x, eye_y1), eye_radius)
    pygame.draw.circle(surf, eye_color, (eye_x, eye_y2), eye_radius)
    pupil_offset = int(0.2 * eye_radius)
    pygame.draw.circle(surf, pupil_color, (eye_x + pupil_offset, eye_y1), pupil_radius)
    pygame.draw.circle(surf, pupil_color, (eye_x + pupil_offset, eye_y2), pupil_radius)
    dorsal_x = int(0.2 * w)
    dorsal_w = int(0.5 * w)
    dorsal_h = max(2, int(h * 0.3))
    pygame.draw.ellipse(surf, fin_color, (dorsal_x, 0, dorsal_w, dorsal_h))
    pectoral_width = max(2, int(0.08 * w))
    pectoral_height = max(2, int(0.15 * h))
    pygame.draw.ellipse(surf, fin_color, (int(0.6 * w), int(0.05 * h), pectoral_width, pectoral_height))
    pygame.draw.ellipse(surf, fin_color, (int(0.6 * w), int(0.8 * h), pectoral_width, pectoral_height))
    tail_width = int(0.3 * w)
    tail_height = int(0.9 * h)
    tail_x = -tail_width // 2
    tail_y = (h - tail_height) // 2
    pygame.draw.ellipse(surf, tail_color, (tail_x, tail_y, tail_width, tail_height))
    if spot_colors:
        for _ in range(random.randint(3, 6)):
            cx = random.uniform(0.2, 0.8) * w
            cy = random.uniform(0.15, 0.4) * h
            rx = random.uniform(0.03, 0.09) * w
            ry = random.uniform(0.03, 0.09) * h
            color = random.choice(spot_colors)
            pygame.draw.ellipse(surf, color, (int(cx - rx / 2), int(cy - ry / 2), int(rx), int(ry)))
            pygame.draw.ellipse(surf, color, (int(cx - rx / 2), int(h - cy - ry / 2), int(rx), int(ry)))
        for _ in range(random.randint(2, 4)):
            cx = random.uniform(0.3, 0.7) * w
            cy = random.uniform(0.05, 0.2) * h
            rx = random.uniform(0.02, 0.06) * w
            ry = random.uniform(0.01, 0.04) * h
            pygame.draw.ellipse(surf, spot_colors[0], (int(cx - rx / 2), int(cy - ry / 2), int(rx), int(ry)))
    return surf.convert_alpha()






def generate_chbos_whale_sprite(length=300, width=96):
    length, width = (max(40, int(length)), max(24, int(width)))
    W, H = (1100, 530)
    surf = pygame.Surface((W, H), pygame.SRCALPHA)
    D = pygame.draw
    D.polygon(surf, (20, 27, 34), [(220, 245), (117, 249), (94, 238), (83, 249), (110, 267), (222, 269)])
    D.polygon(surf, (29, 33, 39), [(110, 252), (52, 150), (25, 134), (50, 208), (78, 261), (43, 317), (22, 376), (55, 365), (118, 274)])
    D.polygon(surf, (234, 122, 29), [(107, 255), (44, 164), (35, 149), (67, 208), (87, 260), (60, 312), (35, 357), (51, 356), (113, 270)])
    D.line(surf, (255, 183, 84), (67, 215), (103, 259), 5)
    D.polygon(surf, (16, 28, 40), [(695, 195), (615, 69), (570, 35), (550, 47), (582, 159)])
    D.polygon(surf, (232, 238, 229), [(666, 307), (569, 424), (535, 493), (570, 473), (723, 325)])
    D.polygon(surf, (184, 197, 207), [(661, 308), (569, 441), (563, 463), (720, 323)])
    for i in range(5):
        D.line(surf, (115, 133, 147), (658 + i * 6, 318), (555 + i * 3, 468 - i * 6), 2)
    contour = [(190, 263), (235, 203), (333, 161), (477, 135), (612, 128), (726, 149), (806, 172), (878, 201), (939, 231), (989, 246), (1016, 263), (989, 284), (938, 303), (878, 326), (803, 351), (713, 374), (591, 392), (451, 384), (325, 348), (235, 303)]
    D.polygon(surf, (13, 22, 29), contour)
    D.ellipse(surf, (21, 73, 112), (216, 142, 782, 241))
    D.polygon(surf, (12, 33, 57), [(220, 260), (295, 195), (430, 157), (575, 151), (706, 159), (828, 207), (987, 250), (966, 280), (822, 323), (698, 368), (542, 376), (370, 332)])
    D.polygon(surf, (231, 234, 226), [(575, 310), (681, 311), (782, 317), (860, 307), (923, 294), (961, 276), (946, 294), (880, 321), (804, 344), (717, 367), (596, 380), (524, 366)])
    D.polygon(surf, (245, 244, 235), [(723, 169), (818, 182), (900, 210), (943, 230), (901, 224), (825, 200), (780, 205), (740, 194)])
    for i in range(8):
        y = 310 + i * 7
        D.arc(surf, (145, 164, 176), (590 - i * 9, y - 25, 345 + i * 7, 68), 0.18, 2.9, 2)
    D.polygon(surf, (16, 103, 158), [(398, 163), (534, 142), (647, 154), (737, 171), (777, 222), (769, 299), (718, 348), (605, 378), (464, 357), (373, 310)])
    D.polygon(surf, (36, 153, 199), [(472, 172), (567, 156), (681, 180), (735, 227), (716, 268), (634, 273), (526, 246)])
    for i in range(9):
        offset = i * 14
        D.arc(surf, (11, 70, 116), (415 + offset, 165, 300, 174), 0.1, 2.8, 2)
    D.polygon(surf, (229, 110, 22), [(210, 253), (239, 212), (303, 181), (389, 163), (448, 182), (452, 229), (436, 306), (390, 335), (303, 331), (242, 297)])
    D.polygon(surf, (247, 151, 44), [(237, 246), (288, 199), (367, 182), (420, 202), (410, 254), (365, 307), (290, 315)])
    for x, y, r in [(289, 218, 8), (327, 272, 7), (376, 197, 5), (401, 294, 6), (261, 273, 4), (347, 304, 4), (419, 239, 3)]:
        D.circle(surf, (36, 33, 33), (x, y), r)
    D.polygon(surf, (16, 22, 27), [(684, 171), (768, 156), (859, 170), (930, 204), (984, 239), (1027, 258), (1018, 282), (962, 304), (887, 321), (808, 321), (746, 292), (690, 245)])
    D.polygon(surf, (36, 43, 47), [(718, 179), (797, 170), (871, 186), (940, 224), (982, 247), (872, 233), (802, 223)])
    D.polygon(surf, (229, 234, 231), [(769, 277), (854, 280), (934, 272), (1001, 262), (979, 282), (906, 309), (847, 318), (795, 313)])
    for i in range(8):
        D.arc(surf, (138, 147, 150), (792, 241 + i * 4, 211, 49 + i * 2), 0.2, 3.1, 2)
    D.circle(surf, (4, 13, 19), (874, 222), 10)
    D.circle(surf, (173, 190, 195), (871, 219), 3)
    D.arc(surf, (7, 13, 18), (895, 252, 124, 38), 3.2, 6.1, 5)
    for j in range(13):
        xx = 741 + j * 17
        yy = int(173 + 0.025 * (xx - 830) ** 2 / 10)
        D.circle(surf, (65, 73, 75), (xx, yy), 3 if j % 3 else 5)
        D.circle(surf, (190, 193, 178), (xx + 1, yy - 1), 1)
    for i in range(8):
        xx = 355 + 49 * i
        yy = 190 + int(15 * math.sin(i * 0.75))
        D.line(surf, (93, 173, 209), (xx, yy), (xx + 27, yy + 9), 2)
    cut = _crop_alpha_surface(surf)
    target_w = length + max(12, round(length * 0.22))
    target_h = max(round(width * 1.8), width + 24)
    return pygame.transform.smoothscale(cut, (target_w, target_h)).convert_alpha()


def _crop_alpha_surface(surface):
    bounds = surface.get_bounding_rect(min_alpha=1)
    if bounds.width > 0 and bounds.height > 0:
        return surface.subsurface(bounds).copy()
    return surface.copy()

def generate_detailed_sea_dragon_sprite(length, width):
    """Детальный дракон-левиафан сверху: бронированные зелёные пластины,
    четыре длинных боковых отростка, руки с когтями, рога и фиолетовые
    перепончатые плавники. Источник рисуется вертикально, затем разворачивается
    так, чтобы базовое направление в игре было вправо.
    """
    W, H = (560, 1000)
    s = pygame.Surface((W, H), pygame.SRCALPHA)
    cx = W // 2
    DARK = (23, 54, 44)
    GREEN_DARK = (38, 89, 62)
    GREEN = (67, 128, 76)
    GREEN_LIGHT = (105, 166, 94)
    SCALE_DARK = (34, 70, 58)
    PURPLE_DARK = (54, 30, 72)
    PURPLE = (105, 51, 107)
    PURPLE_LIGHT = (164, 84, 143)
    ORANGE = (255, 119, 32)
    ORANGE_LIGHT = (255, 197, 75)
    CLAW = (238, 226, 161)
    EYE = (255, 185, 73)

    def poly(color, pts, w=0):
        pygame.draw.polygon(s, color, [(int(x), int(y)) for x, y in pts], w)

    def ell(color, rect, w=0):
        pygame.draw.ellipse(s, color, pygame.Rect(*[int(v) for v in rect]), w)

    def circ(color, pos, r, w=0):
        pygame.draw.circle(s, color, (int(pos[0]), int(pos[1])), max(1, int(r)), w)

    def thick_path(points, widths, outer, inner):
        for i in range(len(points) - 1):
            a = points[i]
            b = points[i + 1]
            ww = max(2, int((widths[i] + widths[i + 1]) / 2))
            pygame.draw.line(s, outer, a, b, ww + 7)
            pygame.draw.line(s, inner, a, b, ww)
            circ(inner, a, ww // 2)
        circ(inner, points[-1], max(2, widths[-1] // 2))
    tendrils = [([(210, 515), (150, 610), (125, 720), (142, 835)], [28, 22, 15, 8], -1), ([(240, 520), (205, 645), (200, 765), (180, 902)], [30, 22, 15, 8], -1), ([(320, 520), (355, 645), (360, 765), (380, 902)], [30, 22, 15, 8], 1), ([(350, 515), (410, 610), (435, 720), (418, 835)], [28, 22, 15, 8], 1)]
    for idx, (pts, widths, side) in enumerate(tendrils):
        thick_path(pts, widths, DARK, GREEN_DARK)
        for j in range(1, 5):
            t = j / 5
            p0, p1 = (pts[min(len(pts) - 2, int(t * (len(pts) - 1)))], pts[min(len(pts) - 1, int(t * (len(pts) - 1)) + 1)])
            local = t * (len(pts) - 1) % 1.0
            x = p0[0] + (p1[0] - p0[0]) * local
            y = p0[1] + (p1[1] - p0[1]) * local
            circ((255, 90, 25, 75), (x, y), 8)
            circ(ORANGE, (x, y), 3)
        tip = pts[-1]
        base = pts[-2]
        if side < 0:
            fin = [(base[0] - 4, base[1] + 20), (tip[0] - 45, tip[1] - 20), (tip[0] - 12, tip[1] + 56), (tip[0] + 12, tip[1] + 8)]
        else:
            fin = [(base[0] + 4, base[1] + 20), (tip[0] + 45, tip[1] - 20), (tip[0] + 12, tip[1] + 56), (tip[0] - 12, tip[1] + 8)]
        poly(PURPLE_DARK, fin)
        inner_fin = [(x + (cx - x) * 0.08, y) for x, y in fin]
        poly(PURPLE, inner_fin)
        pygame.draw.aalines(s, PURPLE_LIGHT, True, fin)
    central = [(cx, 470), (cx + 7, 590), (cx - 4, 710), (cx + 5, 830), (cx, 945)]
    thick_path(central, [72, 58, 42, 27, 13], DARK, GREEN_DARK)
    pygame.draw.line(s, GREEN_LIGHT, (cx - 13, 535), (cx - 8, 870), 4)
    poly(DARK, [(cx - 25, 900), (cx, 975), (cx + 28, 900), (cx + 7, 920), (cx - 7, 920)])
    poly(GREEN_DARK, [(cx - 18, 903), (cx, 960), (cx + 20, 903), (cx + 5, 920), (cx - 5, 920)])
    body = [(cx - 88, 165), (cx - 112, 235), (cx - 101, 340), (cx - 80, 455), (cx - 55, 535), (cx, 565), (cx + 55, 535), (cx + 80, 455), (cx + 101, 340), (cx + 112, 235), (cx + 88, 165)]
    poly(DARK, body)
    body2 = [(cx - 78, 172), (cx - 99, 238), (cx - 88, 334), (cx - 69, 444), (cx - 48, 516), (cx, 545), (cx + 48, 516), (cx + 69, 444), (cx + 88, 334), (cx + 99, 238), (cx + 78, 172)]
    poly(GREEN, body2)
    for i in range(14):
        y = 190 + i * 24
        taper = max(0, i - 6)
        half = 78 - taper * 4
        if i < 4:
            half = 82 + i * 4
        pts = [(cx - half, y), (cx - 18, y - 10), (cx, y - 15), (cx + 18, y - 10), (cx + half, y), (cx + half - 10, y + 13), (cx, y + 8), (cx - half + 10, y + 13)]
        poly(SCALE_DARK, pts)
        poly(GREEN_LIGHT, [(x + (cx - x) * 0.08, yy + 2) for x, yy in pts])
        pygame.draw.aaline(s, (128, 181, 111), (cx - half + 10, y + 1), (cx + half - 10, y + 1))
    for i in range(6):
        y = 238 + i * 35
        poly(PURPLE_DARK, [(cx - 63, y), (cx - 15, y + 10), (cx, y + 18), (cx + 15, y + 10), (cx + 63, y), (cx + 45, y + 13), (cx, y + 28), (cx - 45, y + 13)])
    for sign in (-1, 1):
        shoulder = (cx + sign * 92, 245)
        elbow = (cx + sign * 150, 326)
        wrist = (cx + sign * 167, 405)
        thick_path([shoulder, elbow, wrist], [38, 30, 21], DARK, GREEN)
        pygame.draw.aaline(s, GREEN_LIGHT, (shoulder[0] - sign * 6, shoulder[1]), (wrist[0] - sign * 4, wrist[1] - 5))
        for k in range(3):
            bx = wrist[0] + sign * (3 + k * 7)
            by = wrist[1] + k * 2
            poly(CLAW, [(bx, by), (bx + sign * (20 + k * 2), by + 17 + k * 3), (bx + sign * (7 + k), by + 9)])
        for k in range(4):
            t = (k + 1) / 5
            x = elbow[0] + (wrist[0] - elbow[0]) * t
            y = elbow[1] + (wrist[1] - elbow[1]) * t
            poly(GREEN_LIGHT, [(x, y), (x + sign * 18, y - 5), (x + sign * 4, y + 8)])
    head = [(cx - 92, 120), (cx - 71, 71), (cx - 32, 46), (cx, 39), (cx + 32, 46), (cx + 71, 71), (cx + 92, 120), (cx + 70, 173), (cx, 192), (cx - 70, 173)]
    poly(DARK, head)
    poly(GREEN, [(x + (cx - x) * 0.08, y + 4) for x, y in head])
    for sign in (-1, 1):
        poly(DARK, [(cx + sign * 28, 58), (cx + sign * 52, 10), (cx + sign * 42, 72)])
        poly(GREEN_LIGHT, [(cx + sign * 30, 58), (cx + sign * 48, 17), (cx + sign * 40, 72)])
        poly(DARK, [(cx + sign * 78, 100), (cx + sign * 130, 70), (cx + sign * 86, 121)])
        poly(GREEN_LIGHT, [(cx + sign * 80, 102), (cx + sign * 124, 76), (cx + sign * 86, 117)])
        fin = [(cx + sign * 58, 75), (cx + sign * 112, 37), (cx + sign * 98, 112), (cx + sign * 71, 137)]
        poly(PURPLE_DARK, fin)
        poly(PURPLE, [(x + (cx - x) * 0.05, y + 3) for x, y in fin])
        pygame.draw.aalines(s, PURPLE_LIGHT, True, fin)
    for sign in (-1, 1):
        ex = cx + sign * 34
        ey = 115
        circ((14, 20, 18), (ex, ey), 8)
        circ(EYE, (ex, ey), 5)
        circ((255, 245, 180), (ex - sign * 1, ey - 2), 2)
    for i in range(5):
        y = 82 + i * 17
        half = 47 + i * 7
        poly(SCALE_DARK, [(cx - half, y), (cx, y - 8), (cx + half, y), (cx + half - 8, y + 10), (cx, y + 7), (cx - half + 8, y + 10)])
    for sign in (-1, 1):
        for i in range(11):
            y = 185 + i * 30
            x = cx + sign * (76 - min(i, 7) * 3)
            circ((255, 100, 25, 65), (x, y), 7)
            circ(ORANGE if i % 2 else ORANGE_LIGHT, (x, y), 2.6)
    for sign in (-1, 1):
        for i in range(7):
            y = 180 + i * 42
            x = cx + sign * (91 - max(0, i - 3) * 5)
            poly(GREEN_LIGHT, [(x, y), (x + sign * 20, y + 7), (x, y + 15)])
    s = _crop_alpha_surface(s)
    s = pygame.transform.rotate(s, -90)
    s = _crop_alpha_surface(s)
    length = max(80, int(length))
    width = max(28, int(width))
    margin = max(10, int(max(length, width) * 0.12))
    target_w = length + margin * 2
    target_h = max(width * 3, width + margin * 2)
    return pygame.transform.smoothscale(s, (target_w, target_h)).convert_alpha()



























def generate_zucchini_fish_sprite(length, width):
    """Зелёная рыба-кабачок: длинное салатовое тело, тёмные продольные полосы и крупные треугольные плавники."""
    length = max(18, int(length))
    width = max(8, int(width))
    margin = max(6, int(max(length, width) * 0.22))
    surf_w = length + margin * 2
    surf_h = max(width * 3, width + margin * 2)
    surf = pygame.Surface((surf_w, surf_h), pygame.SRCALPHA)
    cx, cy = (surf_w // 2, surf_h // 2)
    left = margin
    right = left + length

    def P(points):
        return [(int(x), int(y)) for x, y in points]
    body_dark = (32, 175, 78, 255)
    body_main = (165, 225, 18, 255)
    body_light = (194, 247, 56, 255)
    stripe_c = (32, 160, 92, 220)
    fin_main = (48, 190, 82, 235)
    fin_edge = (18, 128, 62, 255)
    eye_white = (245, 248, 245, 255)
    eye_pupil = (20, 22, 20, 255)
    body = [(left, cy), (left + length * 0.07, cy - width * 0.16), (left + length * 0.2, cy - width * 0.34), (left + length * 0.44, cy - width * 0.5), (left + length * 0.72, cy - width * 0.46), (left + length * 0.92, cy - width * 0.22), (right, cy), (left + length * 0.92, cy + width * 0.22), (left + length * 0.72, cy + width * 0.46), (left + length * 0.44, cy + width * 0.5), (left + length * 0.2, cy + width * 0.34), (left + length * 0.07, cy + width * 0.16)]
    pygame.draw.polygon(surf, body_dark, P(body))
    inner = [(left + length * 0.03, cy), (left + length * 0.12, cy - width * 0.12), (left + length * 0.24, cy - width * 0.27), (left + length * 0.46, cy - width * 0.38), (left + length * 0.73, cy - width * 0.35), (left + length * 0.9, cy - width * 0.17), (right - length * 0.02, cy), (left + length * 0.9, cy + width * 0.17), (left + length * 0.73, cy + width * 0.35), (left + length * 0.46, cy + width * 0.38), (left + length * 0.24, cy + width * 0.27), (left + length * 0.12, cy + width * 0.12)]
    pygame.draw.polygon(surf, body_main, P(inner))
    pygame.draw.aalines(surf, fin_edge, True, P(body))
    stripe_offsets = (-0.22, -0.1, 0.0, 0.1, 0.22)
    for frac_y in stripe_offsets:
        pts = []
        for frac_x in (0.1, 0.22, 0.36, 0.5, 0.64, 0.78, 0.9):
            x = left + length * frac_x
            amp = 1.0 - abs(frac_x - 0.55) * 1.25
            y = cy + width * frac_y * amp
            pts.append((x, y))
        pygame.draw.aalines(surf, stripe_c, False, P(pts))
    top_fin = [(left + length * 0.54, cy - width * 0.34), (left + length * 0.7, cy - width * 1.18), (left + length * 0.82, cy - width * 0.3)]
    bottom_fin = [(left + length * 0.54, cy + width * 0.34), (left + length * 0.7, cy + width * 1.18), (left + length * 0.82, cy + width * 0.3)]
    tail_fin = [(left + length * 0.05, cy), (left - length * 0.1, cy - width * 0.34), (left + length * 0.12, cy - width * 0.1), (left - length * 0.1, cy + width * 0.34)]
    for pts in (top_fin, bottom_fin, tail_fin):
        pygame.draw.polygon(surf, fin_main, P(pts))
        pygame.draw.aalines(surf, fin_edge, True, P(pts))
        a, b, c = pts[:3]
        for t in (0.3, 0.55, 0.78):
            sx = a[0] * (1 - t) + c[0] * t
            sy = a[1] * (1 - t) + c[1] * t
            pygame.draw.aaline(surf, body_light, (int(b[0]), int(b[1])), (int(sx), int(sy)))
    for sign in (-1, 1):
        ex = left + length * 0.92
        ey = cy + sign * width * 0.18
        pygame.draw.ellipse(surf, eye_white, pygame.Rect(int(ex - width * 0.18), int(ey - width * 0.1), int(width * 0.36), int(width * 0.2)))
        pygame.draw.circle(surf, eye_pupil, (int(ex + width * 0.02), int(ey)), max(1, int(width * 0.07)))
        pygame.draw.circle(surf, (255, 255, 255, 220), (int(ex + width * 0.07), int(ey - width * 0.03)), max(1, int(width * 0.03)))
    highlight = [(left + length * 0.18, cy), (left + length * 0.3, cy - width * 0.06), (left + length * 0.6, cy - width * 0.08), (left + length * 0.82, cy), (left + length * 0.6, cy + width * 0.08), (left + length * 0.3, cy + width * 0.06)]
    pygame.draw.aalines(surf, body_light, True, P(highlight))
    return surf

def generate_eggplant_fish_sprite(length, width):
    """Фиолетовая рыба-баклажан: округлое вытянутое тело, зелёные плавники и лимонная полоска по центру."""
    length = max(16, int(length))
    width = max(9, int(width))
    margin = max(6, int(max(length, width) * 0.24))
    surf_w = length + margin * 2
    surf_h = max(width * 3, width + margin * 2)
    surf = pygame.Surface((surf_w, surf_h), pygame.SRCALPHA)
    cx, cy = (surf_w // 2, surf_h // 2)
    left = margin
    right = left + length

    def P(points):
        return [(int(x), int(y)) for x, y in points]
    body_outline = (106, 56, 132, 255)
    body_main = (167, 86, 185, 255)
    body_light = (196, 110, 210, 255)
    fin_green = (130, 220, 18, 235)
    fin_green_dark = (28, 170, 86, 255)
    stripe_c = (165, 245, 22, 230)
    spot_c = (124, 54, 138, 180)
    eye_white = (248, 248, 248, 255)
    eye_pupil = (16, 16, 18, 255)
    body = [(left + length * 0.06, cy), (left + length * 0.14, cy - width * 0.22), (left + length * 0.28, cy - width * 0.4), (left + length * 0.5, cy - width * 0.48), (left + length * 0.76, cy - width * 0.4), (left + length * 0.91, cy - width * 0.18), (right, cy), (left + length * 0.91, cy + width * 0.18), (left + length * 0.76, cy + width * 0.4), (left + length * 0.5, cy + width * 0.48), (left + length * 0.28, cy + width * 0.4), (left + length * 0.14, cy + width * 0.22)]
    pygame.draw.polygon(surf, body_outline, P(body))
    inner = [(left + length * 0.1, cy), (left + length * 0.18, cy - width * 0.17), (left + length * 0.3, cy - width * 0.31), (left + length * 0.5, cy - width * 0.38), (left + length * 0.74, cy - width * 0.31), (left + length * 0.88, cy - width * 0.15), (right - length * 0.02, cy), (left + length * 0.88, cy + width * 0.15), (left + length * 0.74, cy + width * 0.31), (left + length * 0.5, cy + width * 0.38), (left + length * 0.3, cy + width * 0.31), (left + length * 0.18, cy + width * 0.17)]
    pygame.draw.polygon(surf, body_main, P(inner))
    pygame.draw.aalines(surf, body_outline, True, P(body))
    midline = [(left + length * 0.18, cy), (left + length * 0.32, cy - width * 0.02), (left + length * 0.48, cy), (left + length * 0.64, cy + width * 0.02), (left + length * 0.79, cy)]
    pygame.draw.lines(surf, stripe_c, False, P(midline), max(1, int(width * 0.1)))
    spots = [(0.24, -0.18), (0.36, -0.1), (0.48, -0.18), (0.62, -0.11), (0.77, -0.05), (0.28, 0.16), (0.42, 0.2), (0.56, 0.12), (0.68, 0.2), (0.82, 0.1)]
    for fx, fy in spots:
        pygame.draw.circle(surf, spot_c, (int(left + length * fx), int(cy + width * fy)), max(1, int(width * 0.07)))
    tail = [(left + length * 0.08, cy), (left - length * 0.14, cy - width * 0.4), (left + length * 0.02, cy - width * 0.06), (left - length * 0.14, cy + width * 0.4)]
    top_fin = [(left + length * 0.62, cy - width * 0.28), (left + length * 0.73, cy - width * 0.95), (left + length * 0.84, cy - width * 0.16)]
    bottom_fin = [(left + length * 0.62, cy + width * 0.28), (left + length * 0.73, cy + width * 0.95), (left + length * 0.84, cy + width * 0.16)]
    mid_top = [(left + length * 0.36, cy - width * 0.18), (left + length * 0.46, cy - width * 0.58), (left + length * 0.57, cy - width * 0.12)]
    mid_bottom = [(left + length * 0.36, cy + width * 0.18), (left + length * 0.46, cy + width * 0.58), (left + length * 0.57, cy + width * 0.12)]
    for pts in (tail, top_fin, bottom_fin, mid_top, mid_bottom):
        pygame.draw.polygon(surf, fin_green, P(pts))
        pygame.draw.aalines(surf, fin_green_dark, True, P(pts))
    for sign in (-1, 1):
        ex = left + length * 0.92
        ey = cy + sign * width * 0.22
        pygame.draw.ellipse(surf, eye_white, pygame.Rect(int(ex - width * 0.16), int(ey - width * 0.09), int(width * 0.32), int(width * 0.18)))
        pygame.draw.circle(surf, eye_pupil, (int(ex + width * 0.01), int(ey)), max(1, int(width * 0.06)))
        pygame.draw.circle(surf, (255, 255, 255, 220), (int(ex + width * 0.05), int(ey - width * 0.02)), max(1, int(width * 0.03)))
    belly = [(left + length * 0.22, cy + width * 0.14), (left + length * 0.42, cy + width * 0.24), (left + length * 0.69, cy + width * 0.2)]
    pygame.draw.aalines(surf, body_light, False, P(belly))
    return surf



def generate_subnautica_creature_sprite(kind, length, width):
    """Графика оставшихся уникальных существ и морского дракона."""
    if kind == 'sea_dragon':
        return generate_detailed_sea_dragon_sprite(length, width)
    if kind == 'chbos_whale':
        return generate_chbos_whale_sprite(length, width)
    if kind == 'zucchini_fish':
        return generate_zucchini_fish_sprite(length, width)
    if kind == 'eggplant_fish':
        return generate_eggplant_fish_sprite(length, width)
    raise ValueError('Неизвестный вид фауны: ' + str(kind))


def generate_weapon_sprite(barrel_length, color, accent_color=None):
    if accent_color is None:
        accent_color = tuple((max(0, c - 90) for c in color))
    grip_width = 8
    grip_height = 14
    body_width = 24
    body_height = 11
    width = body_width + barrel_length + 8
    height = body_height + grip_height + 8
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    grip_x = 6
    grip_y = 4 + body_height - 1
    grip_points = [(grip_x + 3, grip_y), (grip_x + 3 + grip_width, grip_y), (grip_x + 3 + grip_width - 3, grip_y + grip_height), (grip_x, grip_y + grip_height)]
    pygame.draw.polygon(surf, (35, 35, 40), grip_points)
    pygame.draw.polygon(surf, (10, 10, 15), grip_points, 1)
    pygame.draw.arc(surf, (25, 25, 30), (grip_x + 6, grip_y + 1, 12, 10), 0, math.pi, 2)
    body_x = 6
    body_y = 4
    pygame.draw.rect(surf, color, (body_x, body_y, body_width, body_height), border_radius=3)
    darker = tuple((max(0, c - 50) for c in color))
    pygame.draw.rect(surf, darker, (body_x + 2, body_y + 1, body_width - 4, 2))
    pygame.draw.rect(surf, accent_color, (body_x, body_y, body_width, body_height), 2, border_radius=3)
    barrel_x = body_x + body_width - 2
    barrel_height = 5
    barrel_y = body_y + (body_height - barrel_height) // 2
    pygame.draw.rect(surf, (55, 55, 62), (barrel_x, barrel_y, barrel_length, barrel_height))
    pygame.draw.rect(surf, accent_color, (barrel_x, barrel_y, barrel_length, barrel_height), 1)
    muzzle_x = barrel_x + barrel_length - 2
    pygame.draw.rect(surf, color, (muzzle_x, barrel_y - 1, 3, barrel_height + 2))
    pygame.draw.rect(surf, (255, 255, 255), (muzzle_x, barrel_y - 1, 3, barrel_height + 2), 1)
    pygame.draw.rect(surf, accent_color, (body_x + 6, body_y - 2, 2, 3))
    pygame.draw.rect(surf, accent_color, (body_x + body_width - 10, body_y - 2, 2, 3))
    return surf.convert_alpha()

def _fit_size(size, minimum=18):
    return max(minimum, int(size))

def generate_tech_item_sprite(name, size, color, is_weapon=False, weapon_type=''):
    size = _fit_size(size, 18)
    surf = pygame.Surface((size + 12, size + 12), pygame.SRCALPHA)
    cx = surf.get_width() // 2
    cy = surf.get_height() // 2
    dark = _shade(color, -65)
    light = _shade(color, 45)
    lower_name = name.lower()

    def rounded_panel(rect, fill, border):
        pygame.draw.rect(surf, fill, rect, border_radius=5)
        pygame.draw.rect(surf, border, rect, 2, border_radius=5)
        inner = rect.inflate(-6, -6)
        if inner.width > 0 and inner.height > 0:
            pygame.draw.rect(surf, _shade(fill, 18), inner, border_radius=4)
    if is_weapon or 'нож' in lower_name or 'клинок' in lower_name or ('резак' in lower_name) or ('пушка' in lower_name) or ('винтовка' in lower_name):
        length = max(14, size + 4)
        return generate_weapon_sprite(length, color, dark)
    if 'аптеч' in lower_name:
        rect = pygame.Rect(5, 6, size, size - 2)
        rounded_panel(rect, (235, 238, 238), (170, 175, 175))
        pygame.draw.rect(surf, (215, 45, 55), (cx - 3, cy - 9, 6, 18), border_radius=2)
        pygame.draw.rect(surf, (215, 45, 55), (cx - 9, cy - 3, 18, 6), border_radius=2)
    elif 'батар' in lower_name or 'энергояч' in lower_name:
        rect = pygame.Rect(5, 7, size - 2, max(14, size - 4))
        rounded_panel(rect, _shade(color, -10), dark)
        tip = pygame.Rect(rect.right - 3, rect.y + rect.h // 3, 4, rect.h // 3)
        pygame.draw.rect(surf, light, tip, border_radius=2)
        inner = rect.inflate(-8, -8)
        pygame.draw.rect(surf, color, inner, border_radius=3)
        pygame.draw.line(surf, (255, 255, 255), (inner.x + 4, inner.y + inner.h - 4), (inner.right - 4, inner.y + 4), 2)
    elif 'баллон' in lower_name or 'костюм' in lower_name or 'ребриз' in lower_name or ('ласты' in lower_name) or ('компас' in lower_name):
        body = pygame.Rect(cx - size // 4, 6, size // 2, size - 4)
        pygame.draw.rect(surf, color, body, border_radius=6)
        pygame.draw.rect(surf, dark, body, 2, border_radius=6)
        pygame.draw.rect(surf, light, (body.x + 3, body.y + 3, body.w - 6, max(3, body.h // 4)), border_radius=4)
        if 'компас' in lower_name:
            pygame.draw.circle(surf, (250, 250, 245), (cx, cy + 2), max(7, size // 4))
            pygame.draw.circle(surf, dark, (cx, cy + 2), max(7, size // 4), 2)
            pygame.draw.line(surf, (210, 40, 45), (cx, cy + 2), (cx, cy - 7), 2)
            pygame.draw.line(surf, (60, 90, 160), (cx, cy + 2), (cx - 5, cy + 8), 2)
        elif 'ласты' in lower_name:
            pygame.draw.polygon(surf, color, [(cx - 10, cy + 2), (cx - 3, cy - 2), (cx - 2, cy + 10), (cx - 10, cy + 12)])
            pygame.draw.polygon(surf, _shade(color, 25), [(cx + 3, cy - 2), (cx + 10, cy + 2), (cx + 10, cy + 12), (cx + 2, cy + 10)])
            pygame.draw.polygon(surf, dark, [(cx - 10, cy + 2), (cx - 3, cy - 2), (cx - 2, cy + 10), (cx - 10, cy + 12)], 1)
            pygame.draw.polygon(surf, dark, [(cx + 3, cy - 2), (cx + 10, cy + 2), (cx + 10, cy + 12), (cx + 2, cy + 10)], 1)
    elif 'ракета' in lower_name or 'пузыр' in lower_name or 'антисеп' in lower_name or ('кислот' in lower_name) or ('смаз' in lower_name) or ('аэрог' in lower_name):
        if 'ракета' in lower_name:
            body = pygame.Rect(cx - 4, 6, 8, size)
            pygame.draw.rect(surf, color, body, border_radius=4)
            pygame.draw.polygon(surf, light, [(cx, 2), (cx - 6, 10), (cx + 6, 10)])
            pygame.draw.polygon(surf, dark, [(cx - 4, size + 2), (cx - 10, size + 8), (cx - 4, size + 8)])
            pygame.draw.polygon(surf, dark, [(cx + 4, size + 2), (cx + 10, size + 8), (cx + 4, size + 8)])
            pygame.draw.circle(surf, (255, 200, 100), (cx, size + 7), 4)
        else:
            pygame.draw.circle(surf, color, (cx, cy), max(7, size // 3))
            pygame.draw.circle(surf, dark, (cx, cy), max(7, size // 3), 2)
            pygame.draw.circle(surf, light, (cx - 3, cy - 3), max(2, size // 8))
    elif 'провод' in lower_name or 'микросх' in lower_name or 'комплект' in lower_name or ('модуль' in lower_name) or ('скриж' in lower_name) or ('чип' in lower_name) or ('гидролок' in lower_name) or ('дрон' in lower_name):
        rect = pygame.Rect(5, 7, size, size - 2)
        rounded_panel(rect, _shade(color, -10), dark)
        for i in range(4):
            px = rect.x + 5 + i * max(3, rect.w // 5)
            pygame.draw.line(surf, light, (px, rect.y - 1), (px, rect.y + 3), 2)
            pygame.draw.line(surf, light, (px, rect.bottom - 3), (px, rect.bottom + 1), 2)
        inner = rect.inflate(-8, -8)
        pygame.draw.rect(surf, color, inner, border_radius=4)
        pygame.draw.rect(surf, (240, 245, 255), inner, 1, border_radius=4)
        pygame.draw.circle(surf, light, inner.center, max(2, inner.w // 6))
    else:
        rect = pygame.Rect(5, 7, size, size - 2)
        rounded_panel(rect, color, dark)
        pygame.draw.line(surf, light, (rect.x + 5, rect.y + rect.h - 6), (rect.right - 5, rect.y + 6), 2)
        pygame.draw.line(surf, light, (rect.x + 5, rect.y + rect.h - 3), (rect.right - 8, rect.y + 8), 2)
    return surf.convert_alpha()

def generate_building_sprite(name, size, color, wall=False):
    size = _fit_size(size, 20)
    width = size + 12
    height = size + 10
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    dark = _shade(color, -60)
    light = _shade(color, 35)
    lower_name = name.lower()

    def draw_window(rect, tint=(180, 235, 245)):
        pygame.draw.rect(surf, tint, rect, border_radius=3)
        pygame.draw.rect(surf, (95, 145, 165), rect, 1, border_radius=3)
    if wall or 'окно' in lower_name or 'люк' in lower_name or ('переборк' in lower_name):
        panel = pygame.Rect(4, 4, width - 8, height - 8)
        pygame.draw.rect(surf, _shade(color, -8), panel, border_radius=4)
        pygame.draw.rect(surf, dark, panel, 2, border_radius=4)
        inset = panel.inflate(-10, -10)
        if 'окно' in lower_name:
            draw_window(inset)
        elif 'люк' in lower_name:
            pygame.draw.circle(surf, light, panel.center, max(8, min(panel.w, panel.h) // 3), 3)
            pygame.draw.circle(surf, dark, panel.center, max(8, min(panel.w, panel.h) // 3 + 3), 1)
        else:
            pygame.draw.line(surf, light, (panel.x + 4, panel.y + 4), (panel.right - 4, panel.bottom - 4), 2)
            pygame.draw.line(surf, light, (panel.right - 4, panel.y + 4), (panel.x + 4, panel.bottom - 4), 2)
    elif any((key in lower_name for key in ('панель', 'прожектор', 'мачта', 'энергопередатчик'))):
        if 'панель' in lower_name:
            board = pygame.Rect(8, 4, width - 16, height - 14)
            pygame.draw.rect(surf, color, board, border_radius=3)
            pygame.draw.rect(surf, dark, board, 2, border_radius=3)
            for i in range(1, 4):
                x = board.x + i * board.w // 4
                pygame.draw.line(surf, light, (x, board.y + 2), (x, board.bottom - 2), 1)
            for i in range(1, 3):
                y = board.y + i * board.h // 3
                pygame.draw.line(surf, light, (board.x + 2, y), (board.right - 2, y), 1)
            pygame.draw.line(surf, (180, 180, 180), board.midbottom, (width // 2, height - 2), 3)
        elif 'прожектор' in lower_name or 'мачта' in lower_name:
            pygame.draw.line(surf, (175, 180, 185), (width // 2, height - 2), (width // 2, 9), 3)
            pygame.draw.circle(surf, (240, 235, 170), (width // 2, 8), 6)
            pygame.draw.circle(surf, dark, (width // 2, 8), 6, 2)
            pygame.draw.polygon(surf, (245, 245, 180, 70), [(width // 2, 10), (width // 2 - 14, height - 4), (width // 2 + 14, height - 4)])
        else:
            pygame.draw.line(surf, light, (width // 2, height - 3), (width // 2, 10), 3)
            pygame.draw.circle(surf, color, (width // 2, 9), 6)
            pygame.draw.circle(surf, dark, (width // 2, 9), 6, 2)
    elif any((key in lower_name for key in ('реактор', 'станция', 'изготов', 'консоль', 'сканир'))):
        body = pygame.Rect(5, 5, width - 10, height - 10)
        pygame.draw.rect(surf, _shade(color, -10), body, border_radius=5)
        pygame.draw.rect(surf, dark, body, 2, border_radius=5)
        screen = pygame.Rect(body.x + 5, body.y + 5, body.w - 10, max(8, body.h // 2 - 1))
        draw_window(screen, tint=(140, 225, 245))
        pygame.draw.rect(surf, light, (body.x + 8, body.bottom - 8, body.w - 16, 3), border_radius=2)
        if 'реактор' in lower_name:
            pygame.draw.circle(surf, (235, 220, 95), (body.centerx, body.bottom - 10), 4)
            pygame.draw.circle(surf, (120, 95, 30), (body.centerx, body.bottom - 10), 4, 1)
    elif any((key in lower_name for key in ('комната', 'отсек', 'купол', 'обсерват', 'шахта', 'циклоп', 'мотыл', 'краб', 'нептун'))):
        hull = pygame.Rect(4, 6, width - 8, height - 12)
        pygame.draw.rect(surf, color, hull, border_radius=max(6, hull.h // 3))
        pygame.draw.rect(surf, dark, hull, 2, border_radius=max(6, hull.h // 3))
        stripe = pygame.Rect(hull.x + 4, hull.centery - 2, hull.w - 8, 4)
        pygame.draw.rect(surf, light, stripe, border_radius=2)
        if any((key in lower_name for key in ('комната', 'купол', 'обсерват'))):
            for i in range(3):
                wx = hull.x + 6 + i * max(6, hull.w // 4)
                draw_window(pygame.Rect(wx, hull.y + 5, 6, 6))
        elif 'краб' in lower_name:
            pygame.draw.line(surf, dark, (hull.x + 8, hull.bottom - 2), (hull.x + 3, height - 1), 2)
            pygame.draw.line(surf, dark, (hull.right - 8, hull.bottom - 2), (hull.right - 3, height - 1), 2)
        elif 'мотыл' in lower_name:
            pygame.draw.polygon(surf, light, [(hull.right - 2, hull.centery), (hull.right + 4, hull.centery - 5), (hull.right + 4, hull.centery + 5)])
        elif 'циклоп' in lower_name:
            hull.inflate_ip(4, 0)
    elif any((key in lower_name for key in ('шкаф', 'контейнер', 'урна', 'полка', 'горшок', 'грядка', 'аквариум', 'лестниц', 'кровать', 'стул', 'скамья', 'стол', 'автомат'))):
        body = pygame.Rect(6, 6, width - 12, height - 10)
        pygame.draw.rect(surf, color, body, border_radius=4)
        pygame.draw.rect(surf, dark, body, 2, border_radius=4)
        if 'аквариум' in lower_name:
            draw_window(body.inflate(-8, -8))
            pygame.draw.circle(surf, (245, 210, 80), (body.x + 10, body.centery), 2)
        elif 'лестниц' in lower_name:
            x1 = width // 2 - 5
            x2 = width // 2 + 5
            pygame.draw.line(surf, dark, (x1, 4), (x1, height - 4), 2)
            pygame.draw.line(surf, dark, (x2, 4), (x2, height - 4), 2)
            for y in range(8, height - 4, 5):
                pygame.draw.line(surf, light, (x1, y), (x2, y), 2)
        elif any((key in lower_name for key in ('кровать', 'стул', 'скамья', 'стол'))):
            pygame.draw.rect(surf, _shade(color, 25), (body.x + 4, body.y + 4, body.w - 8, body.h // 3), border_radius=3)
    else:
        body = pygame.Rect(6, 6, width - 12, height - 10)
        pygame.draw.rect(surf, color, body, border_radius=4)
        pygame.draw.rect(surf, dark, body, 2, border_radius=4)
        draw_window(body.inflate(-10, -10))
    return surf.convert_alpha()

def _shade(color, amount):
    if amount >= 0:
        return tuple((min(255, c + amount) for c in color))
    return tuple((max(0, c + amount) for c in color))

def _rock_outline(size, rnd, squash=0.9):
    cx = size / 2.0
    cy = size / 2.0
    radius = size * 0.46
    num_points = rnd.randint(7, 10)
    points = []
    for i in range(num_points):
        angle = i / num_points * math.tau
        r = radius * rnd.uniform(0.72, 1.05)
        px = cx + math.cos(angle) * r
        py = cy + math.sin(angle) * r * squash
        points.append((px, py))
    return (cx, cy, points)

def generate_rock_sprite(size, base_color=(110, 110, 110), seed=None):
    rnd = random.Random(seed) if seed is not None else random
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    cx, cy, points = _rock_outline(size, rnd)
    dark = _shade(base_color, -55)
    light = _shade(base_color, 40)
    very_light = _shade(base_color, 75)
    pygame.draw.polygon(surf, dark, points)
    body = [(cx + (p[0] - cx) * 0.94, cy + (p[1] - cy) * 0.94 - size * 0.05) for p in points]
    pygame.draw.polygon(surf, base_color, body)
    highlight = [(cx + (p[0] - cx) * 0.6, cy + (p[1] - cy) * 0.5 - size * 0.1) for p in points]
    pygame.draw.polygon(surf, light, highlight)
    sparkle = [(cx + (p[0] - cx) * 0.35, cy + (p[1] - cy) * 0.3 - size * 0.14) for p in points]
    pygame.draw.polygon(surf, very_light, sparkle)
    for _ in range(rnd.randint(1, 3)):
        a = rnd.uniform(0, math.tau)
        r1 = size * 0.46 * rnd.uniform(0.1, 0.35)
        r2 = size * 0.46 * rnd.uniform(0.45, 0.75)
        x1 = cx + math.cos(a) * r1
        y1 = cy + math.sin(a) * r1
        x2 = cx + math.cos(a) * r2
        y2 = cy + math.sin(a) * r2
        pygame.draw.line(surf, dark, (x1, y1), (x2, y2), 1)
    pygame.draw.polygon(surf, _shade(base_color, -85), points, 1)
    return surf.convert_alpha()

def generate_ore_sprite(size, base_color, vein_colors, vein_count=5, seed=None):
    rnd = random.Random(seed) if seed is not None else random
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    cx, cy, points = _rock_outline(size, rnd)
    dark = _shade(base_color, -55)
    light = _shade(base_color, 35)
    pygame.draw.polygon(surf, dark, points)
    pygame.draw.polygon(surf, base_color, [(cx + (p[0] - cx) * 0.94, cy + (p[1] - cy) * 0.94 - size * 0.04) for p in points])
    pygame.draw.polygon(surf, light, [(cx + (p[0] - cx) * 0.6, cy + (p[1] - cy) * 0.5 - size * 0.09) for p in points])
    used_positions = []
    for _ in range(vein_count):
        placed = False
        for _try in range(12):
            a = rnd.uniform(0, math.tau)
            d = rnd.uniform(0, size * 0.3)
            vx = cx + math.cos(a) * d
            vy = cy + math.sin(a) * d
            too_close = False
            for ux, uy in used_positions:
                if (vx - ux) ** 2 + (vy - uy) ** 2 < (size * 0.18) ** 2:
                    too_close = True
                    break
            if not too_close:
                used_positions.append((vx, vy))
                placed = True
                break
        if not placed:
            continue
        vein_size = rnd.uniform(size * 0.1, size * 0.2)
        color = rnd.choice(vein_colors)
        color_light = _shade(color, 60)
        color_dark = _shade(color, -60)
        shard = [(vx, vy - vein_size), (vx + vein_size * 0.7, vy - vein_size * 0.2), (vx + vein_size * 0.5, vy + vein_size * 0.8), (vx - vein_size * 0.5, vy + vein_size * 0.8), (vx - vein_size * 0.7, vy - vein_size * 0.2)]
        pygame.draw.polygon(surf, color_dark, shard)
        inner = [(vx + (x - vx) * 0.72, vy + (y - vy) * 0.72 - vein_size * 0.1) for x, y in shard]
        pygame.draw.polygon(surf, color, inner)
        pygame.draw.circle(surf, color_light, (int(vx - vein_size * 0.2), int(vy - vein_size * 0.35)), max(1, int(vein_size * 0.25)))
    pygame.draw.polygon(surf, _shade(base_color, -85), points, 1)
    return surf.convert_alpha()

def generate_crystal_sprite(size, color=(0, 255, 255), sparkle=True, seed=None):
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    cx = size / 2.0
    cy = size / 2.0
    r = size * 0.46
    top = (cx, cy - r)
    upper_left = (cx - r * 0.75, cy - r * 0.15)
    upper_right = (cx + r * 0.75, cy - r * 0.15)
    left = (cx - r * 0.85, cy + r * 0.15)
    right = (cx + r * 0.85, cy + r * 0.15)
    bottom = (cx, cy + r)
    dark = _shade(color, -70)
    mid = color
    light = _shade(color, 60)
    bright = _shade(color, 130)
    pygame.draw.polygon(surf, dark, [left, bottom, right, upper_right, upper_left])
    pygame.draw.polygon(surf, mid, [top, upper_left, left, bottom])
    pygame.draw.polygon(surf, dark, [top, upper_right, right, bottom])
    pygame.draw.polygon(surf, light, [top, upper_left, (cx, cy - r * 0.05), upper_right])
    pygame.draw.polygon(surf, bright, [top, (cx - r * 0.3, cy - r * 0.15), (cx - r * 0.1, cy + r * 0.05), (cx, cy - r * 0.05)])
    pygame.draw.polygon(surf, _shade(color, -110), [top, upper_right, right, bottom, left, upper_left], 1)
    if sparkle:
        sx = int(cx - r * 0.35)
        sy = int(cy - r * 0.45)
        pygame.draw.line(surf, (255, 255, 255), (sx - 3, sy), (sx + 3, sy), 1)
        pygame.draw.line(surf, (255, 255, 255), (sx, sy - 3), (sx, sy + 3), 1)
    return surf.convert_alpha()

def generate_seaweed_sprite(size, seed=None):
    rnd = random.Random(seed) if seed is not None else random
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    cx = size / 2.0
    dark = (5, 90, 20)
    mid = (30, 160, 50)
    light = (100, 220, 90)
    bright = (180, 245, 140)
    strands = rnd.randint(3, 4)
    for i in range(strands):
        offset_x = (i - (strands - 1) / 2) * size * 0.18
        phase = rnd.uniform(0, math.tau)
        amplitude = size * rnd.uniform(0.08, 0.14)
        height = size * rnd.uniform(0.65, 0.85)
        points = []
        steps = 8
        for t in range(steps + 1):
            prog = t / steps
            x = cx + offset_x + math.sin(prog * math.pi * 2.2 + phase) * amplitude * (1 - prog * 0.3)
            y = size * 0.92 - prog * height
            points.append((x, y))
        width = max(2, int(size * 0.12))
        for j in range(len(points) - 1):
            pygame.draw.line(surf, dark, points[j], points[j + 1], width + 2)
        for j in range(len(points) - 1):
            pygame.draw.line(surf, mid, points[j], points[j + 1], width)
        for j in range(len(points) - 1):
            pygame.draw.line(surf, light, points[j], points[j + 1], max(1, width // 3))
        tip_x, tip_y = points[-1]
        pygame.draw.circle(surf, light, (int(tip_x), int(tip_y)), max(1, width // 3))
        pygame.draw.circle(surf, bright, (int(tip_x), int(tip_y)), max(1, width // 5))
    return surf.convert_alpha()

def generate_lumpy_sprite(size, color, seed=None, shine=False):
    rnd = random.Random(seed) if seed is not None else random
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    cx = size / 2.0
    cy = size / 2.0
    r = size * 0.42
    dark = _shade(color, -50)
    light = _shade(color, 35)
    bright = _shade(color, 70)
    blobs = rnd.randint(4, 6)
    for i in range(blobs):
        a = i / blobs * math.tau + rnd.uniform(-0.4, 0.4)
        d = r * rnd.uniform(0.15, 0.45)
        bx = cx + math.cos(a) * d
        by = cy + math.sin(a) * d
        br = r * rnd.uniform(0.55, 0.85)
        pygame.draw.circle(surf, dark, (int(bx), int(by)), int(br))
    for i in range(blobs):
        a = i / blobs * math.tau + rnd.uniform(-0.4, 0.4)
        d = r * rnd.uniform(0.15, 0.45)
        bx = cx + math.cos(a) * d
        by = cy + math.sin(a) * d - size * 0.05
        br = r * rnd.uniform(0.5, 0.75)
        pygame.draw.circle(surf, color, (int(bx), int(by)), int(br))
    pygame.draw.circle(surf, light, (int(cx - r * 0.3), int(cy - r * 0.4)), max(1, int(r * 0.35)))
    if shine:
        pygame.draw.circle(surf, bright, (int(cx - r * 0.35), int(cy - r * 0.45)), max(1, int(r * 0.15)))
    return surf.convert_alpha()
_HIGH_RES_RESOURCE_NAMES = {'Камень', 'Титан', 'Медная руда', 'Водоросль', 'Железная руда', 'Золотая руда', 'Алмаз', 'Кварц', 'Уголь', 'Сера', 'Глина', 'Песок', 'Известняк', 'Нефть', 'Соль', 'Серебряная руда', 'Свинец', 'Литий', 'Магнетит', 'Уранинит', 'Рубин', 'Гелевый мешок', 'Сталь', 'Медный провод', 'Стекло', 'Порох', 'Топливо', 'Бетон', 'Металлолом', 'Кислотный гриб', 'Образец водоросли', 'Гроздь семян водоросли', 'Пещерная сера', 'Образец пластинчатого коралла', 'Образец коралловой трубы', 'Зуб сталкера', 'Масляный сок', 'Глубинный гриб', 'Никелевая руда', 'Кианит', 'Ионный куб', 'Газовый пузырь'}

def _stable_preview_seed(name):
    return 10000 + sum(((index + 1) * ord(ch) for index, ch in enumerate(name))) % 50000

def generate_highres_resource_preview(entity, size=280):
    """Redraw a collectible resource at scanner resolution instead of enlarging its tiny icon."""
    name = getattr(entity, 'name', '')
    if name not in _HIGH_RES_RESOURCE_NAMES:
        return None
    size = max(96, int(size))
    color = tuple(getattr(entity, 'color', (150, 150, 150)))[:3]
    seed = _stable_preview_seed(name)
    if name in {'Водоросль', 'Образец водоросли'}:
        return generate_seaweed_sprite(size, seed=seed)
    if name in {'Алмаз', 'Кварц', 'Соль', 'Литий', 'Магнетит', 'Уранинит', 'Рубин', 'Пещерная сера', 'Кианит', 'Ионный куб', 'Зуб сталкера', 'Стекло'}:
        return generate_crystal_sprite(size, color, sparkle=True, seed=seed)
    if 'руда' in name or name in {'Сера', 'Свинец'}:
        rock_base = _shade(color, -85)
        if max(rock_base) < 45:
            rock_base = (82, 84, 88)
        veins = [color, _shade(color, 50), _shade(color, -35)]
        return generate_ore_sprite(size, rock_base, veins, vein_count=8, seed=seed)
    if name in {'Камень', 'Титан', 'Уголь', 'Известняк', 'Бетон', 'Металлолом'}:
        return generate_rock_sprite(size, color, seed=seed)
    if name in {'Сталь', 'Медный провод', 'Топливо'}:
        return generate_tech_item_sprite(name, size - 12, color)
    return generate_lumpy_sprite(size, color, seed=seed, shine=name in {'Нефть', 'Гелевый мешок', 'Кислотный гриб', 'Гроздь семян водоросли', 'Масляный сок', 'Глубинный гриб', 'Газовый пузырь'})

def generate_subnautica_flora_sprite(kind, size, seed=0):
    """Создаёт стилизованный top-down спрайт канонической флоры Subnautica.

    Флора является отдельным декоративным слоем мира и не подменяет
    собираемые ресурсы. Благодаря этому растения не попадают в инвентарь
    сами по себе и не вмешиваются в существующую систему респавна ресурсов.
    """
    rnd = random.Random(seed)
    size = max(12, int(size))
    canvas = max(size + 10, int(size * 1.35))
    surf = pygame.Surface((canvas, canvas), pygame.SRCALPHA)
    cx = canvas // 2
    cy = canvas // 2
    r = max(4, size // 2)

    def pt(x, y):
        return (int(x), int(y))

    def line(color, a, b, width=1):
        pygame.draw.line(surf, color, pt(*a), pt(*b), max(1, int(width)))

    def circle(color, pos, radius, width=0):
        pygame.draw.circle(surf, color, pt(*pos), max(1, int(radius)), width)

    def ellipse(color, rect, width=0):
        pygame.draw.ellipse(surf, color, pygame.Rect(*(int(v) for v in rect)), width)

    def polygon(color, points, width=0):
        pygame.draw.polygon(surf, color, [pt(x, y) for x, y in points], width)

    def glow(pos, radius, color, alpha=55):
        g = pygame.Surface((canvas, canvas), pygame.SRCALPHA)
        for mul, a in ((2.1, alpha // 3), (1.55, alpha // 2), (1.15, alpha)):
            pygame.draw.circle(g, (*color, max(1, int(a))), pt(*pos), max(1, int(radius * mul)))
        surf.blit(g, (0, 0))
    if kind in ('acid_mushroom', 'deep_shroom'):
        deep = kind == 'deep_shroom'
        cap = (110, 72, 190) if not deep else (56, 54, 92)
        rim = (190, 120, 245) if not deep else (110, 102, 165)
        spots = (235, 185, 255) if not deep else (160, 155, 220)
        glow((cx, cy), r * 0.55, rim, 42)
        ellipse((45, 38, 60), (cx - r * 0.76, cy - r * 0.42, r * 1.52, r * 0.88))
        ellipse(cap, (cx - r * 0.7, cy - r * 0.5, r * 1.4, r * 0.86))
        ellipse(rim, (cx - r * 0.62, cy - r * 0.42, r * 1.24, r * 0.65), 2)
        for _ in range(7):
            a = rnd.uniform(0, math.tau)
            d = rnd.uniform(0.1, 0.48) * r
            circle(spots, (cx + math.cos(a) * d, cy + math.sin(a) * d * 0.62), max(1, r * 0.07))
        circle((65, 45, 80), (cx, cy + r * 0.03), r * 0.16)
    elif kind == 'creepvine':
        for strand in range(5):
            phase = rnd.uniform(0, math.tau)
            pts = []
            for i in range(9):
                t = i / 8
                ang = phase + t * 1.25 + strand * 0.16
                rad = r * (0.12 + t * 0.82)
                pts.append((cx + math.cos(ang) * rad, cy + math.sin(ang) * rad))
            for a, b in zip(pts, pts[1:]):
                line((20, 75, 30), a, b, max(3, r * 0.16))
                line((78, 158, 52), a, b, max(1, r * 0.09))
            tip = pts[-1]
            for j in range(3):
                a = phase + 2.0 + j * 2.0
                p = (tip[0] + math.cos(a) * r * 0.12, tip[1] + math.sin(a) * r * 0.12)
                glow(p, r * 0.08, (255, 185, 35), 32)
                circle((255, 188, 35), p, r * 0.07)
                circle((255, 235, 110), (p[0] - 1, p[1] - 1), r * 0.025)
    elif kind == 'blood_grass':
        for i in range(18):
            a = rnd.uniform(0, math.tau)
            length = rnd.uniform(r * 0.48, r * 0.95)
            bend = rnd.uniform(-0.22, 0.22)
            base = (cx + math.cos(a) * r * 0.1, cy + math.sin(a) * r * 0.1)
            tip = (cx + math.cos(a + bend) * length, cy + math.sin(a + bend) * length)
            line((85, 25, 28), base, tip, max(2, r * 0.1))
            line((170, 48, 55), base, tip, max(1, r * 0.045))
    elif kind == 'blue_palm':
        circle((60, 95, 115), (cx, cy), r * 0.22)
        for i in range(8):
            a = i / 8 * math.tau + rnd.uniform(-0.08, 0.08)
            mid = (cx + math.cos(a) * r * 0.48, cy + math.sin(a) * r * 0.48)
            tip = (cx + math.cos(a) * r * 0.88, cy + math.sin(a) * r * 0.88)
            line((75, 190, 120), (cx, cy), mid, max(2, r * 0.11))
            line((30, 92, 155), mid, tip, max(3, r * 0.16))
            circle((55, 145, 190), tip, r * 0.1)
        circle((160, 225, 95), (cx, cy), r * 0.11)
    elif kind in ('writhing_weed', 'ghost_weed'):
        ghost = kind == 'ghost_weed'
        base = (65, 110, 115) if ghost else (60, 70, 145)
        stem = (90, 190, 110) if ghost else (145, 75, 185)
        tipc = (235, 80, 90) if ghost else (155, 245, 120)
        circle(base, (cx, cy), r * 0.25)
        for i in range(9 if not ghost else 6):
            a = i / (9 if not ghost else 6) * math.tau + rnd.uniform(-0.2, 0.2)
            length = rnd.uniform(r * 0.45, r * 0.95)
            tip = (cx + math.cos(a) * length, cy + math.sin(a) * length)
            line((45, 45, 70), (cx, cy), tip, max(3, r * 0.13))
            line(stem, (cx, cy), tip, max(1, r * 0.07))
            glow(tip, r * 0.055, tipc, 28)
            circle(tipc, tip, r * 0.05)
    elif kind in ('spotted_reeds', 'spotted_dockleaf'):
        dock = kind == 'spotted_dockleaf'
        leaf_c = (84, 145, 72) if not dock else (84, 112, 125)
        spot_c = (230, 235, 190) if not dock else (220, 205, 95)
        count = 7 if not dock else 5
        for i in range(count):
            a = i / count * math.tau + rnd.uniform(-0.18, 0.18)
            length = rnd.uniform(r * 0.52, r * 0.92)
            half = r * (0.13 if not dock else 0.18)
            tip = (cx + math.cos(a) * length, cy + math.sin(a) * length)
            nx, ny = (-math.sin(a) * half, math.cos(a) * half)
            polygon((45, 72, 50), [(cx, cy), (tip[0] + nx, tip[1] + ny), tip, (tip[0] - nx, tip[1] - ny)])
            polygon(leaf_c, [(cx, cy), (tip[0] + nx * 0.72, tip[1] + ny * 0.72), tip, (tip[0] - nx * 0.72, tip[1] - ny * 0.72)])
            for frac in (0.45, 0.7):
                p = (cx + (tip[0] - cx) * frac, cy + (tip[1] - cy) * frac)
                circle(spot_c, p, max(1, r * 0.035))
    elif kind in ('redwort', 'violet_beau'):
        violet = kind == 'violet_beau'
        stem_c = (110, 58, 62) if not violet else (85, 70, 120)
        tip_c = (220, 68, 62) if not violet else (185, 105, 235)
        for i in range(10):
            a = i / 10 * math.tau + rnd.uniform(-0.12, 0.12)
            length = rnd.uniform(r * 0.38, r * 0.86)
            tip = (cx + math.cos(a) * length, cy + math.sin(a) * length)
            line(stem_c, (cx, cy), tip, max(2, r * 0.09))
            circle(tip_c, tip, r * 0.09)
        circle((70, 55, 75), (cx, cy), r * 0.16)
    elif kind in ('tiger_plant', 'spiked_horn_grass'):
        tiger = kind == 'tiger_plant'
        base_c = (95, 40, 25) if tiger else (135, 112, 72)
        stem_c = (195, 60, 40) if tiger else (190, 125, 65)
        spike_c = (245, 210, 85) if tiger else (180, 225, 85)
        circle(base_c, (cx, cy), r * 0.22)
        for i in range(8):
            a = i / 8 * math.tau + rnd.uniform(-0.13, 0.13)
            length = rnd.uniform(r * 0.48, r * 0.92)
            tip = (cx + math.cos(a) * length, cy + math.sin(a) * length)
            line((70, 40, 28), (cx, cy), tip, max(4, r * 0.15))
            line(stem_c, (cx, cy), tip, max(2, r * 0.09))
            for frac in (0.45, 0.72):
                p = (cx + (tip[0] - cx) * frac, cy + (tip[1] - cy) * frac)
                pa = a + math.pi / 2
                q = (p[0] + math.cos(pa) * r * 0.12, p[1] + math.sin(pa) * r * 0.12)
                line(spike_c, p, q, max(1, r * 0.035))
    elif kind in ('mushroom_tree', 'jellyshroom'):
        jelly = kind == 'jellyshroom'
        outer = (70, 105, 150) if not jelly else (96, 55, 145)
        inner = (95, 165, 175) if not jelly else (205, 95, 210)
        bright = (135, 205, 190) if not jelly else (245, 155, 250)
        if jelly:
            glow((cx, cy), r * 0.7, (180, 80, 235), 46)
        for i in range(6):
            a = i / 6 * math.tau + rnd.uniform(-0.12, 0.12)
            rr = r * rnd.uniform(0.48, 0.75)
            p = (cx + math.cos(a) * rr * 0.35, cy + math.sin(a) * rr * 0.35)
            circle((45, 55, 72), p, rr * 0.55)
            circle(outer, p, rr * 0.5)
            circle(inner, (p[0] - rr * 0.06, p[1] - rr * 0.06), rr * 0.38)
            circle(bright, (p[0] - rr * 0.14, p[1] - rr * 0.14), rr * 0.12)
        circle((35, 34, 50), (cx, cy), r * 0.22)
        if jelly:
            circle((235, 175, 250), (cx, cy), r * 0.13, 2)
    elif kind == 'pygmy_fan':
        for i in range(7):
            a = i / 7 * math.tau
            p1 = (cx + math.cos(a - 0.28) * r * 0.18, cy + math.sin(a - 0.28) * r * 0.18)
            p2 = (cx + math.cos(a) * r * 0.88, cy + math.sin(a) * r * 0.88)
            p3 = (cx + math.cos(a + 0.28) * r * 0.18, cy + math.sin(a + 0.28) * r * 0.18)
            polygon((95, 55, 145), [p1, p2, p3])
            line((210, 120, 245), (cx, cy), p2, max(1, r * 0.035))
        glow((cx, cy), r * 0.22, (175, 95, 235), 28)
        circle((185, 110, 240), (cx, cy), r * 0.15)
    elif kind == 'bulb_bush':
        for i in range(9):
            a = i / 9 * math.tau + rnd.uniform(-0.15, 0.15)
            d = r * rnd.uniform(0.25, 0.72)
            p = (cx + math.cos(a) * d, cy + math.sin(a) * d)
            br = r * rnd.uniform(0.18, 0.3)
            glow(p, br * 0.7, (90, 120, 240), 25)
            circle((58, 63, 125), p, br + 2)
            circle((96, 110, 205), p, br)
            circle((155, 185, 245), (p[0] - br * 0.25, p[1] - br * 0.25), br * 0.24)
    elif kind in ('bloodvine', 'bloodroot'):
        root = kind == 'bloodroot'
        strands = 7 if root else 5
        for i in range(strands):
            a = i / strands * math.tau + rnd.uniform(-0.18, 0.18)
            length = rnd.uniform(r * 0.55, r * 0.98)
            mid = (cx + math.cos(a + 0.22) * length * 0.5, cy + math.sin(a + 0.22) * length * 0.5)
            tip = (cx + math.cos(a) * length, cy + math.sin(a) * length)
            line((50, 45, 58), (cx, cy), mid, max(5, r * 0.18 if root else r * 0.13))
            line((206, 215, 205), (cx, cy), mid, max(3, r * 0.11 if root else r * 0.08))
            line((196, 205, 200), mid, tip, max(2, r * 0.08))
            if not root or i % 2 == 0:
                p = (cx + (tip[0] - cx) * 0.72, cy + (tip[1] - cy) * 0.72)
                glow(p, r * 0.09, (220, 55, 55), 34)
                circle((180, 35, 45), p, r * 0.08)
                circle((245, 90, 75), (p[0] - 1, p[1] - 1), r * 0.03)
    elif kind == 'gabe_feather':
        for i in range(6):
            a = i / 6 * math.tau + rnd.uniform(-0.12, 0.12)
            tip = (cx + math.cos(a) * r * 0.92, cy + math.sin(a) * r * 0.92)
            line((80, 65, 105), (cx, cy), tip, max(3, r * 0.12))
            for frac in (0.38, 0.55, 0.72, 0.86):
                p = (cx + (tip[0] - cx) * frac, cy + (tip[1] - cy) * frac)
                side = a + math.pi / 2
                length = r * (0.18 * (1 - frac * 0.35))
                q1 = (p[0] + math.cos(side) * length, p[1] + math.sin(side) * length)
                q2 = (p[0] - math.cos(side) * length, p[1] - math.sin(side) * length)
                line((170, 100, 185), p, q1, max(1, r * 0.035))
                line((170, 100, 185), p, q2, max(1, r * 0.035))
    elif kind == 'membraintree':
        glow((cx, cy), r * 0.62, (70, 155, 245), 35)
        circle((40, 70, 95), (cx, cy), r * 0.72)
        circle((110, 175, 215, 145), (cx, cy), r * 0.65)
        for _ in range(8):
            a = rnd.uniform(0, math.tau)
            d = rnd.uniform(0, r * 0.42)
            p = (cx + math.cos(a) * d, cy + math.sin(a) * d)
            circle((215, 80, 185), p, r * 0.1)
            circle((245, 165, 225), (p[0] - 1, p[1] - 1), r * 0.035)
        circle((205, 235, 250), (cx - r * 0.18, cy - r * 0.2), r * 0.18, 2)
    elif kind == 'anchor_pod':
        glow((cx, cy - r * 0.05), r * 0.56, (60, 210, 220), 45)
        circle((40, 80, 85), (cx, cy - r * 0.05), r * 0.6)
        circle((72, 170, 175), (cx, cy - r * 0.05), r * 0.53)
        circle((115, 220, 205), (cx - r * 0.15, cy - r * 0.2), r * 0.2)
        for i in range(7):
            a = i / 7 * math.tau
            start = (cx + math.cos(a) * r * 0.43, cy - r * 0.05 + math.sin(a) * r * 0.43)
            end = (cx + math.cos(a) * r * 0.9, cy + math.sin(a) * r * 0.9)
            line((100, 38, 70), start, end, max(2, r * 0.065))
    elif kind == 'cave_bush':
        for i in range(13):
            a = i / 13 * math.tau + rnd.uniform(-0.12, 0.12)
            length = rnd.uniform(r * 0.4, r * 0.9)
            tip = (cx + math.cos(a) * length, cy + math.sin(a) * length)
            line((65, 45, 85), (cx, cy), tip, max(3, r * 0.11))
            line((155, 85, 190), (cx, cy), tip, max(1, r * 0.055))
            circle((205, 120, 225), tip, r * 0.055)
    elif kind == 'rouge_cradle':
        glow((cx, cy), r * 0.38, (255, 190, 55), 38)
        circle((55, 80, 125), (cx, cy), r * 0.62)
        for i in range(10):
            a = i / 10 * math.tau
            outer = (cx + math.cos(a) * r * 0.82, cy + math.sin(a) * r * 0.82)
            line((135, 45, 45), (cx, cy), outer, max(2, r * 0.09))
        circle((120, 50, 50), (cx, cy), r * 0.42, 2)
        circle((245, 190, 60), (cx, cy), r * 0.24)
        circle((255, 235, 135), (cx - r * 0.06, cy - r * 0.06), r * 0.08)
    elif kind == 'drooping_stinger':
        glow((cx, cy), r * 0.42, (110, 220, 145), 24)
        for i in range(7):
            a = i / 7 * math.tau + rnd.uniform(-0.15, 0.15)
            d = rnd.uniform(r * 0.25, r * 0.68)
            p = (cx + math.cos(a) * d, cy + math.sin(a) * d)
            circle((40, 85, 58, 190), p, r * 0.22)
            circle((105, 190, 105, 180), (p[0] - 2, p[1] - 2), r * 0.15)
            line((80, 180, 105), p, (cx, cy), max(1, r * 0.04))
    elif kind == 'sulfur_plant':
        circle((115, 72, 40), (cx, cy), r * 0.56)
        circle((185, 105, 42), (cx, cy), r * 0.46)
        polygon((245, 220, 105), [(cx - r * 0.15, cy - r * 0.06), (cx, cy - r * 0.44), (cx + r * 0.16, cy - r * 0.05), (cx + r * 0.42, cy + r * 0.12), (cx, cy + r * 0.28), (cx - r * 0.42, cy + r * 0.12)])
        circle((50, 35, 28), (cx, cy), r * 0.17)
    elif kind == 'sea_crown':
        glow((cx, cy), r * 0.42, (70, 190, 255), 42)
        for i in range(8):
            a = i / 8 * math.tau
            tip = (cx + math.cos(a) * r * 0.9, cy + math.sin(a) * r * 0.9)
            line((115, 210, 80), (cx, cy), tip, max(3, r * 0.11))
            circle((225, 215, 70), tip, r * 0.06)
        circle((60, 115, 155), (cx, cy), r * 0.42)
        circle((95, 185, 225), (cx, cy), r * 0.34)
        circle((185, 235, 245), (cx - r * 0.09, cy - r * 0.11), r * 0.09)
    elif kind == 'regress_shell':
        for i, frac in enumerate((0.78, 0.6, 0.43, 0.27)):
            circle((130 + 20 * i, 45 + 12 * i, 42 + 10 * i), (cx, cy), r * frac, max(2, int(r * 0.09)))
        for i in range(8):
            a = i / 8 * math.tau
            tip = (cx + math.cos(a) * r * 0.88, cy + math.sin(a) * r * 0.88)
            line((220, 145, 70), (cx, cy), tip, max(1, r * 0.035))
            circle((245, 235, 205), tip, r * 0.04)
    elif kind == 'eye_stalk':
        for i in range(5):
            a = i / 5 * math.tau + rnd.uniform(-0.2, 0.2)
            length = rnd.uniform(r * 0.55, r * 0.95)
            tip = (cx + math.cos(a) * length, cy + math.sin(a) * length)
            mid = (cx + math.cos(a + 0.28) * length * 0.52, cy + math.sin(a + 0.28) * length * 0.52)
            line((58, 32, 82), (cx, cy), mid, max(4, r * 0.14))
            line((125, 60, 155), (cx, cy), mid, max(2, r * 0.08))
            line((120, 58, 155), mid, tip, max(2, r * 0.07))
            glow(tip, r * 0.08, (245, 205, 70), 34)
            circle((245, 210, 75), tip, r * 0.07)
            circle((70, 55, 45), tip, r * 0.025)
    elif kind == 'furled_papyrus':
        for i in range(7):
            a = i / 7 * math.tau + rnd.uniform(-0.14, 0.14)
            tip = (cx + math.cos(a) * r * 0.78, cy + math.sin(a) * r * 0.78)
            line((75, 105, 72), (cx, cy), tip, max(3, r * 0.1))
            q = (tip[0] + math.cos(a + 1.8) * r * 0.18, tip[1] + math.sin(a + 1.8) * r * 0.18)
            line((165, 165, 95), tip, q, max(2, r * 0.07))
            circle((205, 190, 110), q, r * 0.045)
    else:
        circle((55, 92, 72), (cx, cy), r * 0.18)
        for i in range(8):
            a = i / 8 * math.tau + rnd.uniform(-0.15, 0.15)
            tip = (cx + math.cos(a) * r * 0.86, cy + math.sin(a) * r * 0.86)
            line((85, 155, 95), (cx, cy), tip, max(2, r * 0.08))
            circle((145, 205, 125), tip, r * 0.06)
    return surf.convert_alpha()

def generate_diver_sprite(size=30, suit_color=(30, 100, 180), accent_color=(255, 200, 50), tank_color=(200, 205, 215)):
    width = int(size * 2.4)
    height = int(size * 1.7)
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    center_x = width // 2
    center_y = height // 2
    body_length = int(size * 1.05)
    body_width = int(size * 0.62)
    flipper_length = int(size * 0.7)
    flipper_width = int(size * 0.24)
    flipper_x = center_x - body_length // 2 - flipper_length + 6
    for sign in (-1, 1):
        flipper_y = center_y + sign * (body_width // 2 + flipper_width // 2 - 1) - flipper_width // 2
        pygame.draw.ellipse(surf, (18, 18, 28), (flipper_x, flipper_y, flipper_length, flipper_width))
        pygame.draw.ellipse(surf, (70, 70, 90), (flipper_x, flipper_y, flipper_length, flipper_width), 1)
    tank_length = int(size * 0.6)
    tank_width = int(size * 0.2)
    tank_x = center_x - tank_length // 2 - 2
    for sign in (-1, 1):
        tank_y = center_y + sign * (body_width // 2 + tank_width // 2 + 1) - tank_width // 2
        pygame.draw.rect(surf, tank_color, (tank_x, tank_y, tank_length, tank_width), border_radius=3)
        pygame.draw.rect(surf, (90, 95, 110), (tank_x, tank_y, tank_length, tank_width), 1, border_radius=3)
        pygame.draw.line(surf, (255, 255, 255), (tank_x + 3, tank_y + 1), (tank_x + tank_length - 3, tank_y + 1), 1)
    pygame.draw.ellipse(surf, suit_color, (center_x - body_length // 2, center_y - body_width // 2, body_length, body_width))
    pygame.draw.line(surf, accent_color, (center_x - body_length // 3, center_y), (center_x + body_length // 3, center_y), 2)
    pygame.draw.ellipse(surf, accent_color, (center_x - body_length // 2, center_y - body_width // 2, body_length, body_width), 2)
    arm_length = int(size * 0.5)
    arm_width = int(size * 0.16)
    for sign in (-1, 1):
        arm_y = center_y + sign * (body_width // 2 + arm_width // 2 - 2) - arm_width // 2
        arm_x = center_x + body_length // 4
        pygame.draw.ellipse(surf, suit_color, (arm_x, arm_y, arm_length, arm_width))
        pygame.draw.ellipse(surf, accent_color, (arm_x, arm_y, arm_length, arm_width), 1)
    head_radius = int(size * 0.3)
    head_x = center_x + body_length // 2 - 1
    head_y = center_y
    pygame.draw.circle(surf, (235, 195, 160), (head_x, head_y), head_radius)
    pygame.draw.circle(surf, (140, 100, 70), (head_x, head_y), head_radius, 1)
    mask_width = int(head_radius * 1.6)
    mask_height = int(head_radius * 1.3)
    mask_x = head_x - mask_width // 2 + 2
    mask_y = head_y - mask_height // 2
    pygame.draw.ellipse(surf, (70, 180, 220), (mask_x, mask_y, mask_width, mask_height))
    pygame.draw.ellipse(surf, (20, 60, 90), (mask_x, mask_y, mask_width, mask_height), 2)
    pygame.draw.ellipse(surf, (200, 240, 255), (mask_x + 2, mask_y + 2, max(2, mask_width // 3), max(2, mask_height // 4)))
    pygame.draw.line(surf, (255, 200, 0), (head_x - 2, head_y - head_radius + 2), (center_x - 2, center_y - body_width // 2 - 3), 2)
    return surf.convert_alpha()

def point_in_polygon(x, y, points):
    """Return True when a point is inside an arbitrary polygon."""
    inside = False
    j = len(points) - 1
    for i in range(len(points)):
        xi, yi = points[i]
        xj, yj = points[j]
        intersects = (yi > y) != (yj > y) and x < (xj - xi) * (y - yi) / (yj - yi + 1e-09) + xi
        if intersects:
            inside = not inside
        j = i
    return inside

def make_blob_points(cx, cy, radius_x, radius_y, seed, point_count=22, jitter=0.14):
    """Create a stable irregular outline for a biome."""
    rnd = random.Random(seed)
    points = []
    for i in range(point_count):
        angle = i / point_count * math.tau
        radius_mul = rnd.uniform(1.0 - jitter, 1.0 + jitter)
        wave = 1.0 + math.sin(angle * 3 + seed * 0.17) * 0.05 + math.sin(angle * 5 + seed * 0.31) * 0.035
        px = cx + math.cos(angle) * radius_x * radius_mul * wave
        py = cy + math.sin(angle) * radius_y * radius_mul * wave
        px = max(0, min(MAP_SIZE - 1, px))
        py = max(0, min(MAP_SIZE - 1, py))
        points.append((px, py))
    return points

def make_corridor_polygon(x1, y1, x2, y2, width):
    """Create a thick polygon corridor between two points."""
    dx = x2 - x1
    dy = y2 - y1
    distance = math.hypot(dx, dy)
    if distance <= 0.001:
        return make_blob_points(x1, y1, width, width, seed=int(x1 + y1), point_count=12, jitter=0.08)
    ux = dx / distance
    uy = dy / distance
    nx = -uy
    ny = ux
    half = width / 2.0
    sx = x1 - ux * half
    sy = y1 - uy * half
    ex = x2 + ux * half
    ey = y2 + uy * half
    return [(sx + nx * half, sy + ny * half), (sx - nx * half, sy - ny * half), (ex - nx * half, ey - ny * half), (ex + nx * half, ey + ny * half)]

class Biome:

    def __init__(self, name, color, x1, y1, x2, y2, objects, animals, passable=True, polygon=None, map_label=None, cave_enabled=False, priority=10, clip_biome=None):
        self.name = name
        self.color = color
        self.objects = objects
        self.animals = animals
        self.flora = []
        self.passable = passable
        self.map_label = map_label
        self.cave_enabled = cave_enabled
        self.priority = priority
        self.clip_biome = clip_biome
        if polygon:
            self.polygon = [(float(px), float(py)) for px, py in polygon]
            xs = [p[0] for p in self.polygon]
            ys = [p[1] for p in self.polygon]
            self.rect = (int(min(xs)), int(min(ys)), int(max(xs)), int(max(ys)))
        else:
            self.polygon = None
            self.rect = (x1, y1, x2, y2)

    def contains(self, x, y):
        x1, y1, x2, y2 = self.rect
        if not (x1 <= x <= x2 and y1 <= y <= y2):
            return False
        if self.polygon is not None:
            if not point_in_polygon(x, y, self.polygon):
                return False
        if self.clip_biome is not None:
            if not self.clip_biome.contains(x, y):
                return False
        return True

    def center(self):
        if self.polygon:
            x = sum((p[0] for p in self.polygon)) / len(self.polygon)
            y = sum((p[1] for p in self.polygon)) / len(self.polygon)
            return (x, y)
        x1, y1, x2, y2 = self.rect
        return ((x1 + x2) / 2.0, (y1 + y2) / 2.0)

    def area(self):
        if self.polygon:
            result = 0.0
            count = len(self.polygon)
            for i in range(count):
                x1, y1 = self.polygon[i]
                x2, y2 = self.polygon[(i + 1) % count]
                result += x1 * y2 - x2 * y1
            return abs(result) / 2.0
        x1, y1, x2, y2 = self.rect
        return (x2 - x1 + 1) * (y2 - y1 + 1)

    def random_point(self, rng=None):
        if rng is None:
            rng = random
        x1, y1, x2, y2 = self.rect
        for _ in range(400):
            x = rng.uniform(x1, x2)
            y = rng.uniform(y1, y2)
            if self.contains(x, y):
                return (x, y)
        return self.center()

    def map_points(self):
        if self.polygon:
            return self.polygon
        x1, y1, x2, y2 = self.rect
        return [(x1, y1), (x2 + 1, y1), (x2 + 1, y2 + 1), (x1, y2 + 1)]

    def shift(self, dx, dy):
        x1, y1, x2, y2 = self.rect
        self.rect = (x1 + dx, y1 + dy, x2 + dx, y2 + dy)
        if self.polygon:
            self.polygon = [(x + dx, y + dy) for x, y in self.polygon]

def _bio_normalize_kind(value):
    return str(value).lower().replace(' ', '_').replace('-', '_')

def _bio_stable_seed(value):
    return sum(((i + 1) * ord(c) for i, c in enumerate(str(value))))

def _bio_light_art(original, species, is_plant=False, seed=0):
    """Рендерит ТОЛЬКО светящиеся участки, строго отдельно от тела.

    На выходе картинка с полями для ореола, которая может независимо
    поворачиваться вместе с исходной рыбой.
    """
    if original is None:
        return None
    bounds = original.get_bounding_rect(min_alpha=125)
    if bounds.width == 0 or bounds.height == 0:
        return None
    kind = _bio_normalize_kind(species)
    w, h = original.get_size()
    pad = max(12, min(33, int(max(bounds.width, bounds.height) * 0.14)))
    dims = (w + pad * 2, h + pad * 2)
    aura = pygame.Surface(dims, pygame.SRCALPHA)
    core = pygame.Surface(dims, pygame.SRCALPHA)
    cx, cy = (pad + bounds.centerx, pad + bounds.centery)

    def pos(u, v):
        return (round(pad + bounds.left + bounds.width * u), round(pad + bounds.top + bounds.height * v))

    def dot(x, y, radius, color, white=None, intensity=1.0):
        x, y = (int(x), int(y))
        r = max(1, int(radius))
        for m, a in ((5, 9), (3.2, 19), (2.05, 42), (1.35, 65)):
            pygame.draw.circle(aura, (*color, min(255, round(a * intensity))), (x, y), max(1, round(r * m)))
        pygame.draw.circle(core, (*color, min(255, round(205 * intensity))), (x, y), r + 1)
        if white is None:
            white = tuple((min(255, round(c * 0.4 + 255 * 0.6)) for c in color))
        pygame.draw.circle(core, (*white, min(255, round(249 * intensity))), (x, y), r)

    def orb(u, v, relative_radius, color, intensity=1.0):
        x, y = pos(u, v)
        dot(x, y, max(1, round(min(bounds.width, bounds.height) * relative_radius)), color, intensity=intensity)

    def path(vertices, color, thickness=1, intensity=1.0):
        if len(vertices) < 2:
            return
        points = [pos(x, y) for x, y in vertices]
        thickness = max(1, round(thickness))
        for broad, alpha in ((6, 14), (3, 32)):
            pygame.draw.lines(aura, (*color, round(alpha * intensity)), False, points, thickness + broad)
        pygame.draw.lines(core, (*color, round(205 * intensity)), False, points, thickness + 1)
        pale = tuple((min(255, round(c * 0.55 + 255 * 0.45)) for c in color))
        pygame.draw.lines(core, (*pale, round(225 * intensity)), False, points, max(1, thickness // 2))

    def luminous_oval(u, v, rw, rh, color, fill=False, intensity=1.0):
        x, y = pos(u, v)
        rx = max(2, round(bounds.width * rw))
        ry = max(2, round(bounds.height * rh))
        rectangle = pygame.Rect(x - rx, y - ry, rx * 2, ry * 2)
        broad = rectangle.inflate(max(4, rx // 2), max(4, ry // 2))
        pygame.draw.ellipse(aura, (*color, round(26 * intensity)), broad)
        pygame.draw.ellipse(aura, (*color, round(56 * intensity)), rectangle)
        pygame.draw.ellipse(core, (*color, round((105 if fill else 220) * intensity)), rectangle, 0 if fill else max(1, min(rx, ry) // 3))
        if not fill:
            inner = rectangle.inflate(-max(2, rx // 3), -max(2, ry // 3))
            if inner.width > 1 and inner.height > 1:
                pygame.draw.ellipse(core, (245, 250, 255, round(75 * intensity)), inner, 1)

    def color_pixels(predicate, glow_color, region=None, stride=1, max_points=300):
        """Выделить точно окрашенный орган из уже нарисованного спрайта.

        Сохраняет форму глазных склер, семенных гроздьев, капюшонов и узоров.
        """
        x0, y0, x1, y1 = (bounds.left, bounds.top, bounds.right, bounds.bottom)
        if region is not None:
            ux0, vy0, ux1, vy1 = region
            x0 = max(x0, int(bounds.left + bounds.width * ux0))
            x1 = min(x1, int(bounds.left + bounds.width * ux1))
            y0 = max(y0, int(bounds.top + bounds.height * vy0))
            y1 = min(y1, int(bounds.top + bounds.height * vy1))
        chosen = []
        for yy in range(y0, y1, stride):
            for xx in range(x0, x1, stride):
                r, g, b, a = original.get_at((xx, yy))
                if a >= 170 and predicate(r, g, b):
                    px, py = (xx + pad, yy + pad)
                    chosen.append((px, py))
                    pygame.draw.rect(core, (*glow_color, 245), (px, py, stride, stride))
        if chosen:
            step = max(1, len(chosen) // max_points)
            for i in range(0, len(chosen), step):
                px, py = chosen[i]
                pygame.draw.circle(aura, (*glow_color, 25), (px, py), max(3, min(7, round(min(bounds.size) / 10))))
        return len(chosen)
    if is_plant:
        if kind in ('blood_grass', 'redwort', 'tiger_plant', 'spiked_horn_grass', 'mushroom_tree', 'gabe_feather', 'regress_shell', 'furled_papyrus'):
            return None
        if kind in ('гроздь_семян_водоросли', 'creepvine_seed_cluster'):
            luminous_oval(0.5, 0.5, 0.36, 0.34, (255, 208, 32), fill=True, intensity=1.15)
            for u, v in ((0.3, 0.37), (0.65, 0.36), (0.55, 0.67)):
                orb(u, v, 0.1, (255, 220, 72), 1.1)
        elif kind in ('кислотный_гриб', 'acid_mushroom_item'):
            luminous_oval(0.5, 0.5, 0.36, 0.29, (190, 104, 242), fill=True, intensity=0.6)
        elif kind in ('глубинный_гриб', 'deep_shroom_item'):
            luminous_oval(0.5, 0.5, 0.34, 0.27, (127, 109, 220), fill=True, intensity=0.65)
        elif kind in ('масляный_сок', 'blood_oil'):
            for u, v in ((0.38, 0.45), (0.59, 0.38), (0.52, 0.64)):
                orb(u, v, 0.12, (229, 53, 68), 0.9)
        elif kind in ('гелевый_мешок', 'gel_sack'):
            for u, v in ((0.3, 0.4), (0.58, 0.31), (0.63, 0.63), (0.39, 0.69)):
                orb(u, v, 0.085, (200, 120, 255), 0.76)
        elif kind == 'creepvine':
            found = color_pixels(lambda r, g, b: r > 202 and g > 125 and (14 < b < 175) and (r > g * 0.94) and (g > b + 40), (255, 218, 66), stride=1, max_points=170)
            if not found:
                for u, v in ((0.33, 0.32), (0.38, 0.35), (0.66, 0.6), (0.68, 0.55)):
                    orb(u, v, 0.045, (255, 195, 24), 1.15)
        elif kind in ('acid_mushroom', 'deep_shroom'):
            col = (176, 100, 235) if kind == 'acid_mushroom' else (133, 102, 207)
            luminous_oval(0.5, 0.45, 0.31, 0.29, col)
            for u, v in ((0.32, 0.37), (0.52, 0.32), (0.68, 0.45), (0.45, 0.6)):
                orb(u, v, 0.04, col, 0.55)
        elif kind == 'jellyshroom':
            for u, v in ((0.3, 0.39), (0.51, 0.29), (0.71, 0.4), (0.38, 0.65), (0.65, 0.7)):
                luminous_oval(u, v, 0.12, 0.09, (195, 96, 242), fill=True)
        elif kind == 'bulb_bush':
            for u, v in ((0.28, 0.34), (0.53, 0.25), (0.72, 0.38), (0.35, 0.62), (0.64, 0.7), (0.5, 0.5)):
                luminous_oval(u, v, 0.105, 0.09, (65, 145, 255), fill=True)
                orb(u - 0.03, v - 0.025, 0.035, (120, 225, 255))
        elif kind in ('bloodvine', 'bloodroot'):
            for u, v in ((0.21, 0.37), (0.42, 0.23), (0.77, 0.35), (0.69, 0.74), (0.35, 0.78)):
                orb(u, v, 0.07, (230, 42, 62))
        elif kind == 'eye_stalk':
            for u, v in ((0.22, 0.35), (0.53, 0.18), (0.81, 0.43), (0.65, 0.83), (0.23, 0.72)):
                orb(u, v, 0.06, (255, 209, 45))
        elif kind == 'sea_crown':
            luminous_oval(0.5, 0.5, 0.25, 0.25, (62, 175, 255), fill=True)
            for i in range(8):
                a = i * math.tau / 8
                orb(0.5 + 0.39 * math.cos(a), 0.5 + 0.39 * math.sin(a), 0.026, (235, 220, 105))
        elif kind == 'drooping_stinger':
            for u, v in ((0.3, 0.35), (0.52, 0.29), (0.69, 0.46), (0.38, 0.68), (0.6, 0.7)):
                luminous_oval(u, v, 0.1, 0.09, (93, 232, 128), fill=True, intensity=0.8)
        elif kind in ('ghost_weed', 'writhing_weed'):
            color = (123, 220, 251) if kind == 'ghost_weed' else (146, 236, 123)
            for i in range(7):
                a = math.tau * i / 7
                orb(0.5 + 0.38 * math.cos(a), 0.5 + 0.38 * math.sin(a), 0.028, color, 0.7)
        elif kind in ('membraintree', 'anchor_pod'):
            color = (255, 107, 217) if kind == 'membraintree' else (85, 231, 232)
            luminous_oval(0.5, 0.48, 0.3, 0.3, color, fill=True, intensity=0.64)
            for u, v in ((0.42, 0.35), (0.61, 0.43), (0.5, 0.59)):
                orb(u, v, 0.05, color)
        elif kind == 'rouge_cradle':
            luminous_oval(0.5, 0.5, 0.2, 0.2, (255, 207, 76), fill=True)
        elif kind == 'pygmy_fan':
            for i in range(7):
                a = math.tau * i / 7
                path(((0.5, 0.5), (0.5 + 0.4 * math.cos(a), 0.5 + 0.4 * math.sin(a))), (183, 105, 238), 1, 0.65)
        elif kind in ('spotted_reeds', 'spotted_dockleaf'):
            col = (210, 228, 155) if kind == 'spotted_reeds' else (223, 207, 99)
            for i in range(8):
                a = math.tau * i / 8
                for rad in (0.21, 0.32):
                    orb(0.5 + rad * math.cos(a), 0.5 + rad * math.sin(a), 0.025, col, 0.6)
        elif kind in ('blue_palm', 'cave_bush', 'violet_beau', 'sulfur_plant'):
            col = {'blue_palm': (73, 158, 240), 'cave_bush': (191, 130, 221), 'violet_beau': (189, 105, 235), 'sulfur_plant': (235, 210, 83)}[kind]
            if kind == 'sulfur_plant':
                luminous_oval(0.5, 0.5, 0.2, 0.12, col, fill=True, intensity=0.44)
            else:
                for i in range(8):
                    a = i * math.tau / 8
                    orb(0.5 + 0.36 * math.cos(a), 0.5 + 0.36 * math.sin(a), 0.028, col, 0.57)
        else:
            return None
    elif kind == 'sea_dragon':
        for u, v in ((0.92, 0.35), (0.92, 0.65)):
            orb(u, v, 0.045, (255, 172, 50))
        for i in range(7):
            u = 0.15 + i * 0.092
            for vv in (0.3, 0.7):
                orb(u, vv, 0.02, (255, 104, 38), 0.72)
    elif kind == 'eggplant_fish':
        luminous_oval(0.76, 0.49, 0.07, 0.21, (93, 194, 255))
        for u in (0.29, 0.43, 0.58):
            orb(u, 0.32, 0.03, (138, 176, 252), 0.6)
    elif kind == 'zucchini_fish':
        orb(0.79, 0.46, 0.053, (194, 234, 113), 0.82)
    else:
        return None
    result = pygame.Surface(dims, pygame.SRCALPHA)
    result.blit(aura, (0, 0))
    result.blit(core, (0, 0))
    return result.convert_alpha()

def generate_bioluminescence_sprite(original, species, is_plant=False, seed=0):
    return _bio_light_art(original, species, is_plant, seed)

class FloraType:

    def __init__(self, name, kind, size, seed=0):
        self.name = name
        self.kind = kind
        self.size = int(size)
        self.seed = int(seed)
        self.image = generate_subnautica_flora_sprite(kind, self.size, seed=self.seed)
        self.glow_image = generate_bioluminescence_sprite(self.image, kind, is_plant=True, seed=self.seed)

class ObjectType:

    def __init__(self, name, color, size, shape='circle', image_path=None, volume=1, is_buildable=False, is_wall=False, edible=False, hunger_value=0, thirst_value=0, is_weapon=False, damage=0, weapon_range=0, weapon_type=''):
        self.name = name
        self.color = color
        self.size = size
        self.shape = shape
        self.volume = volume
        self.is_buildable = is_buildable
        self.is_wall = is_wall
        self.edible = edible
        self.hunger_value = hunger_value
        self.thirst_value = thirst_value
        self.is_weapon = is_weapon
        self.damage = damage
        self.weapon_range = weapon_range
        self.weapon_type = weapon_type
        self.image = None
        if image_path and os.path.exists(image_path):
            self.image = load_image_with_proportions(image_path, size)

class AnimalType:

    def __init__(self, name, color, size, shape='circle', speed=0.05, behavior='neutral', image_path=None, volume=1, hp=10, damage=0, edible=False, hunger_value=0, thirst_value=0, fish_style=False, fish_length=None, fish_width=None, body_color=None, spine_color=None, belly_color=None, fin_color=None, spot_colors=None, tail_color=(255, 165, 0), sprite_kind=None, schooling=False, fear_distance=None, zigzag_strength=0.0, speed_variation=0.12, flee_speed_boost=1.65):
        self.name = name
        self.color = color
        self.size = size
        self.shape = shape
        self.speed = speed
        self.behavior = behavior
        self.volume = volume
        self.hp = hp
        self.damage = damage
        self.edible = edible
        self.hunger_value = hunger_value
        self.thirst_value = thirst_value
        self.fish_style = fish_style or sprite_kind is not None
        self.schooling = schooling
        self.sprite_kind = sprite_kind
        self.fear_distance = float(fear_distance) if fear_distance is not None else 2.7 if behavior == 'prey' else 0.0
        self.zigzag_strength = float(zigzag_strength)
        self.speed_variation = float(speed_variation)
        self.flee_speed_boost = float(flee_speed_boost)
        self.fish_length = float(fish_length if fish_length is not None else max(20, size * 3))
        self.fish_width = float(fish_width if fish_width is not None else max(6, self.fish_length / 6))
        self.is_weapon = False
        self.is_buildable = False
        self.weapon_range = 0
        self.weapon_type = ''
        self.image = None
        if image_path and os.path.exists(image_path):
            self.image = load_image_with_proportions(image_path, size)
        elif sprite_kind is not None:
            length = fish_length if fish_length is not None else max(20, size * 3)
            width = fish_width if fish_width is not None else max(6, length // 6)
            self.image = generate_subnautica_creature_sprite(sprite_kind, length, width)
        elif fish_style:
            length = fish_length if fish_length is not None else max(20, size * 3)
            width = fish_width if fish_width is not None else max(6, length // 6)
            body = body_color if body_color is not None else color
            spine = spine_color if spine_color is not None else tuple((max(0, c - 60) for c in body))
            belly = belly_color if belly_color is not None else tuple((min(255, c + 40) for c in body))
            fins = fin_color if fin_color is not None else spine
            spots = spot_colors if spot_colors is not None else [spine]
            self._hd_fish_palette = (body, spine, belly, fins, spots, tail_color)
            self.image = generate_pike_style_sprite(length, width, body, spine, belly, fins, spots, tail_color)
        self.glow_image = generate_bioluminescence_sprite(self.image, self.sprite_kind or self.name)
        self.glow_rotated_cache = {}
        self.rotated_cache = {}

    def get_rotated_glow(self, angle):
        if self.glow_image is None:
            return None
        angle = quantize_angle(angle)
        result = self.glow_rotated_cache.get(angle)
        if result is None:
            result = pygame.transform.rotate(self.glow_image, angle)
            self.glow_rotated_cache[angle] = result
        return result

    def get_rotated(self, angle):
        if self.image is None:
            return None
        angle = quantize_angle(angle)
        image = self.rotated_cache.get(angle)
        if image is None:
            image = pygame.transform.rotate(self.image, angle)
            self.rotated_cache[angle] = image
        return image

class Recipe:

    def __init__(self, name, ingredients, result, result_count=1, station='Изготовитель', category='Разное'):
        self.name = name
        self.ingredients = ingredients
        self.result = result
        self.result_count = max(1, int(result_count))
        self.station = station
        self.category = category

    def can_craft(self, inventory_counts):
        for item, needed in self.ingredients.items():
            if inventory_counts.get(item, 0) < needed:
                return False
        return True

    def craft(self, inventory, inventory_counts):
        if not self.can_craft(inventory_counts):
            return None
        for needed_item, needed_count in self.ingredients.items():
            removed = 0
            for index, item in enumerate(inventory):
                if removed >= needed_count:
                    break
                if item == needed_item:
                    inventory[index] = None
                    removed += 1
            inventory_counts[needed_item] -= needed_count
            if inventory_counts[needed_item] <= 0:
                del inventory_counts[needed_item]
        return self.result

def init_assets():
    global rock_type
    global titan_type
    global copper_type
    global seaweed_type
    global iron_type
    global gold_type
    global diamond_type
    global quartz_type
    global coal_type
    global sulfur_type
    global clay_type
    global sand_type
    global limestone_type
    global oil_type
    global salt_type
    global silver_type
    global lead_type
    global lithium_type
    global magnetite_type
    global uraninite_type
    global ruby_type
    global gel_sack_type
    global kyanite_type
    global steel_type
    global provod_type
    global glass_type
    global gunpowder_type
    global fuel_type
    global concrete_type
    global zucchini_fish_type
    global eggplant_fish_type
    global sea_dragon_type
    global chbos_whale_type
    global wall_type
    global floor_type
    global habitat_builder_type
    global fins_type, reinforced_suit, water_filtration_suit, rebreather_type
    global first_aid_kit, flashlight_type, seaglide_type, flare_type
    global fire_extinguisher, air_bladder, scanner_type, pathfinder_tool
    global repair_tool, repair_kit, propulsion_cannon, repulsion_cannon, stasis_rifle
    global laser_cutter, creature_decoy, scanner_range, beacon_type
    global spotlight_type, floodlight_type, solar_panel_type, grav_trap
    global exterior_growbed, plant_pot, indoor_growbed, plant_shelf
    global composite_pot, chic_pot
    global bed_type, bench_type, chair_type, vending_machine, coffee_machine
    global water_filtration_machine, medical_fabricator, scanner_room_type
    global radio_type
    global seamoth_type, cyclops_type
    global START_ITEM_REGISTRY
    global mad_moss_item, antibiotic_syringe
    global laser_pistol
    global underwater_rifle
    global plasma_cannon
    global lang_cannon
    global schweer_cannon
    global diamond_cutter
    global flamethrower
    global golden_pistol
    global recipes
    global biomes
    global flora_types
    global surface_flora_profiles
    global cave_flora_profiles
    rock_type = ObjectType('Камень', (100, 100, 100), 20, 'rect')
    titan_type = ObjectType('Титан', (205, 224, 216), 35, 'rect')
    copper_type = ObjectType('Медная руда', (211, 127, 34), 19, 'rect')
    mad_moss_item = ObjectType('Бешеный мох', (65, 179, 79), 24, 'circle', edible=True, hunger_value=1, thirst_value=4)
    mad_moss_item.image = pygame.transform.smoothscale(generate_mad_moss_sprite(150), (26, 26)).convert_alpha()
    antibiotic_syringe = ObjectType('Шприц с антибиотиком', (108, 232, 205), 26, 'rect')
    antibiotic_syringe.image = generate_tech_item_sprite('Шприц с антибиотиком', 28, (108, 232, 205))
    seaweed_type = ObjectType('Водоросль', (0, 200, 0), 18, 'circle', edible=True, hunger_value=5, thirst_value=5)
    iron_type = ObjectType('Железная руда', (169, 169, 169), 25, 'rect')
    gold_type = ObjectType('Золотая руда', (255, 215, 0), 30, 'rect')
    diamond_type = ObjectType('Алмаз', (0, 255, 255), 40, 'rect')
    quartz_type = ObjectType('Кварц', (255, 255, 255), 15, 'rect')
    coal_type = ObjectType('Уголь', (50, 50, 50), 22, 'rect')
    sulfur_type = ObjectType('Сера', (255, 255, 0), 18, 'rect')
    clay_type = ObjectType('Глина', (139, 69, 19), 12, 'rect')
    sand_type = ObjectType('Песок', (194, 178, 128), 10, 'rect')
    limestone_type = ObjectType('Известняк', (200, 200, 200), 16, 'rect')
    oil_type = ObjectType('Нефть', (30, 30, 30), 20, 'rect')
    salt_type = ObjectType('Соль', (225, 235, 245), 14, 'rect')
    silver_type = ObjectType('Серебряная руда', (195, 205, 215), 18, 'rect')
    lead_type = ObjectType('Свинец', (80, 90, 105), 20, 'rect')
    lithium_type = ObjectType('Литий', (220, 210, 235), 17, 'rect')
    magnetite_type = ObjectType('Магнетит', (65, 65, 78), 20, 'rect')
    uraninite_type = ObjectType('Уранинит', (110, 190, 45), 22, 'rect')
    ruby_type = ObjectType('Рубин', (230, 40, 65), 21, 'rect')
    gel_sack_type = ObjectType('Гелевый мешок', (155, 70, 190), 18, 'circle')
    steel_type = ObjectType('Сталь', (180, 180, 200), 18, 'rect')
    provod_type = ObjectType('Медный провод', (211, 127, 34), 12, 'circle')
    glass_type = ObjectType('Стекло', (200, 230, 255), 14, 'rect')
    gunpowder_type = ObjectType('Порох', (60, 60, 60), 12, 'circle')
    fuel_type = ObjectType('Топливо', (90, 60, 20), 16, 'rect')
    concrete_type = ObjectType('Бетон', (170, 170, 160), 20, 'rect')
    steel_type.image = generate_tech_item_sprite('Сталь', steel_type.size, steel_type.color)
    provod_type.image = generate_tech_item_sprite('Медный провод', provod_type.size, provod_type.color)
    glass_type.image = generate_crystal_sprite(glass_type.size, (190, 225, 245), sparkle=True, seed=214)
    gunpowder_type.image = generate_lumpy_sprite(gunpowder_type.size, (65, 65, 68), seed=215)
    fuel_type.image = generate_tech_item_sprite('Топливо', fuel_type.size, fuel_type.color)
    concrete_type.image = generate_rock_sprite(concrete_type.size, (170, 170, 160), seed=216)
    metal_salvage_type = ObjectType('Металлолом', (125, 135, 145), 24, 'rect')
    acid_mushroom_item = ObjectType('Кислотный гриб', (145, 80, 205), 15, 'circle')
    creepvine_sample_type = ObjectType('Образец водоросли', (55, 165, 70), 18, 'circle')
    creepvine_seed_type = ObjectType('Гроздь семян водоросли', (235, 185, 40), 17, 'circle')
    cave_sulfur_type = ObjectType('Пещерная сера', (245, 220, 65), 16, 'circle')
    table_coral_type = ObjectType('Образец пластинчатого коралла', (220, 85, 105), 17, 'rect')
    coral_tube_type = ObjectType('Образец коралловой трубы', (185, 115, 85), 17, 'rect')
    blood_oil_type = ObjectType('Масляный сок', (165, 35, 45), 19, 'circle')
    deep_shroom_item = ObjectType('Глубинный гриб', (70, 62, 105), 16, 'circle')
    nickel_type = ObjectType('Никелевая руда', (125, 160, 150), 20, 'rect')
    kyanite_type = ObjectType('Кианит', (70, 125, 245), 22, 'rect')
    ion_cube_type = ObjectType('Ионный куб', (65, 245, 150), 22, 'rect')
    rock_type.image = generate_rock_sprite(20, (115, 115, 120), seed=101)
    titan_type.image = generate_rock_sprite(35, (205, 222, 218), seed=102)
    copper_type.image = generate_ore_sprite(19, (95, 95, 100), [(211, 127, 34), (240, 160, 60), (170, 95, 20)], vein_count=4, seed=103)
    seaweed_type.image = generate_seaweed_sprite(18, seed=104)
    iron_type.image = generate_ore_sprite(25, (115, 115, 120), [(190, 190, 195), (140, 140, 145), (235, 235, 240)], vein_count=5, seed=105)
    gold_type.image = generate_ore_sprite(30, (95, 85, 70), [(255, 215, 0), (255, 235, 120), (220, 180, 0)], vein_count=6, seed=106)
    diamond_type.image = generate_crystal_sprite(40, (0, 240, 255), sparkle=True, seed=107)
    quartz_type.image = generate_crystal_sprite(15, (225, 240, 255), sparkle=False, seed=108)
    coal_type.image = generate_rock_sprite(22, (45, 45, 50), seed=109)
    sulfur_type.image = generate_ore_sprite(18, (105, 95, 70), [(255, 255, 0), (255, 240, 80), (220, 220, 40)], vein_count=4, seed=110)
    clay_type.image = generate_lumpy_sprite(12, (139, 69, 19), seed=111)
    sand_type.image = generate_lumpy_sprite(10, (194, 178, 128), seed=112)
    limestone_type.image = generate_rock_sprite(16, (215, 215, 205), seed=113)
    oil_type.image = generate_lumpy_sprite(20, (28, 28, 32), seed=114, shine=True)
    salt_type.image = generate_crystal_sprite(14, (225, 240, 255), sparkle=False, seed=201)
    silver_type.image = generate_ore_sprite(18, (95, 100, 110), [(205, 215, 225), (170, 180, 195)], vein_count=4, seed=202)
    lead_type.image = generate_ore_sprite(20, (70, 75, 82), [(105, 115, 130), (65, 70, 85)], vein_count=4, seed=203)
    lithium_type.image = generate_crystal_sprite(17, (225, 215, 240), sparkle=True, seed=204)
    magnetite_type.image = generate_crystal_sprite(20, (65, 65, 80), sparkle=True, seed=205)
    uraninite_type.image = generate_crystal_sprite(22, (105, 190, 45), sparkle=True, seed=206)
    ruby_type.image = generate_crystal_sprite(21, (235, 40, 65), sparkle=True, seed=207)
    gel_sack_type.image = generate_lumpy_sprite(18, (145, 65, 180), seed=208, shine=True)
    metal_salvage_type.image = generate_rock_sprite(24, (125, 135, 145), seed=220)
    acid_mushroom_item.image = generate_lumpy_sprite(15, (145, 80, 205), seed=221, shine=True)
    creepvine_sample_type.image = generate_seaweed_sprite(18, seed=222)
    creepvine_seed_type.image = generate_lumpy_sprite(17, (235, 185, 40), seed=223, shine=True)
    cave_sulfur_type.image = generate_crystal_sprite(16, (245, 220, 65), seed=224)
    table_coral_type.image = generate_lumpy_sprite(17, (220, 85, 105), seed=225)
    coral_tube_type.image = generate_lumpy_sprite(17, (185, 115, 85), seed=226)
    blood_oil_type.image = generate_lumpy_sprite(19, (165, 35, 45), seed=228, shine=True)
    deep_shroom_item.image = generate_lumpy_sprite(16, (70, 62, 105), seed=229, shine=True)
    nickel_type.image = generate_ore_sprite(20, (90, 105, 102), [(125, 175, 160)], seed=230)
    kyanite_type.image = generate_crystal_sprite(22, (70, 125, 245), seed=231)
    ion_cube_type.image = generate_crystal_sprite(22, (65, 245, 150), seed=232)
    for glowing_item in (seaweed_type, gel_sack_type, acid_mushroom_item, creepvine_sample_type, creepvine_seed_type, blood_oil_type, deep_shroom_item, table_coral_type, coral_tube_type):
        glowing_item.glow_image = generate_bioluminescence_sprite(glowing_item.image, glowing_item.name, is_plant=True)
    flora_types = {'acid_mushroom': FloraType('Кислотный гриб', 'acid_mushroom', 22, 3001), 'deep_shroom': FloraType('Глубинный гриб', 'deep_shroom', 20, 3002), 'creepvine': FloraType('Ползучая водоросль', 'creepvine', 68, 3003), 'blood_grass': FloraType('Кровавая трава', 'blood_grass', 24, 3004), 'blue_palm': FloraType('Синяя пальма', 'blue_palm', 38, 3005), 'writhing_weed': FloraType('Извивающаяся трава', 'writhing_weed', 28, 3006), 'spotted_reeds': FloraType('Пятнистый тростник', 'spotted_reeds', 28, 3007), 'redwort': FloraType('Краснотравник', 'redwort', 22, 3008), 'violet_beau': FloraType('Фиолетовая красавица', 'violet_beau', 24, 3009), 'tiger_plant': FloraType('Растение-тигр', 'tiger_plant', 28, 3010), 'spiked_horn_grass': FloraType('Шипорогая трава', 'spiked_horn_grass', 26, 3011), 'mushroom_tree': FloraType('Грибное дерево', 'mushroom_tree', 76, 3012), 'jellyshroom': FloraType('Медузогриб', 'jellyshroom', 66, 3013), 'pygmy_fan': FloraType('Карликовый веер', 'pygmy_fan', 28, 3014), 'bulb_bush': FloraType('Луковичный куст', 'bulb_bush', 48, 3015), 'spotted_dockleaf': FloraType('Пятнистый щавель', 'spotted_dockleaf', 28, 3016), 'bloodvine': FloraType('Кровавая лоза', 'bloodvine', 62, 3017), 'bloodroot': FloraType('Кровавый корень', 'bloodroot', 68, 3018), 'ghost_weed': FloraType('Призрачная трава', 'ghost_weed', 36, 3019), 'gabe_feather': FloraType('Перо Гейба', 'gabe_feather', 30, 3020), 'membraintree': FloraType('Мембранное дерево', 'membraintree', 42, 3021), 'anchor_pod': FloraType('Якорная капсула', 'anchor_pod', 58, 3022), 'cave_bush': FloraType('Пещерный куст', 'cave_bush', 30, 3023), 'rouge_cradle': FloraType('Красная колыбель', 'rouge_cradle', 30, 3024), 'drooping_stinger': FloraType('Свисающая жгучка', 'drooping_stinger', 40, 3025), 'sulfur_plant': FloraType('Серное растение', 'sulfur_plant', 26, 3026), 'sea_crown': FloraType('Морская корона', 'sea_crown', 30, 3027), 'regress_shell': FloraType('Регрессирующая раковина', 'regress_shell', 26, 3028), 'eye_stalk': FloraType('Глазастый стебель', 'eye_stalk', 32, 3029), 'furled_papyrus': FloraType('Свёрнутый папирус', 'furled_papyrus', 28, 3030)}
    zucchini_fish_type = AnimalType('Рыба-кабачок', (132, 208, 22), 13, speed=0.07, behavior='prey', hp=8, edible=True, hunger_value=44, thirst_value=4, fish_length=57, fish_width=15, sprite_kind='zucchini_fish', schooling=True, fear_distance=2.8, zigzag_strength=0.17, speed_variation=0.17, flee_speed_boost=1.8)
    eggplant_fish_type = AnimalType('Рыба-баклажан', (166, 86, 186), 12, speed=0.075, behavior='prey', hp=6, edible=True, hunger_value=30, thirst_value=2, fish_length=33, fish_width=15, sprite_kind='eggplant_fish', schooling=True, fear_distance=2.9, zigzag_strength=0.29, speed_variation=0.17, flee_speed_boost=1.9)
    sea_dragon_type = AnimalType('Морской дракон-левиафан', (60, 120, 75), 240, speed=0.065, behavior='predator', hp=800, damage=30, edible=False, fish_length=300, fish_width=35, sprite_kind='sea_dragon')
    chbos_whale_type = AnimalType(WHALE_NAME, (30, 122, 185), 240, speed=WHALE_SPEED / 60.0, behavior='predator', hp=WHALE_HP, damage=0, edible=True, hunger_value=90, thirst_value=90, fish_length=sea_dragon_type.fish_length, fish_width=96, sprite_kind='chbos_whale', volume=12, speed_variation=0.0)

    def tech(name, color=(90, 155, 185), size=20, weapon=False, damage=0, weapon_range=0, weapon_type=''):
        obj = ObjectType(name, color, size, 'rect', is_weapon=weapon, damage=damage, weapon_range=weapon_range, weapon_type=weapon_type)
        obj.image = generate_tech_item_sprite(name, size, color, is_weapon=weapon, weapon_type=weapon_type)
        return obj

    def buildable(name, color=(105, 145, 155), wall=False, size=30):
        obj = ObjectType(name, color, size, 'rect', is_buildable=True, is_wall=wall)
        obj.image = generate_building_sprite(name, size, color, wall=wall)
        obj.requires_habitat_builder = True
        return obj
    titanium_ingot = tech('Титановый слиток', (185, 205, 205))
    fiber_mesh = tech('Сетчатое волокно', (120, 170, 110))
    silicone_rubber = tech('Силиконовая резина', (170, 185, 175))
    bleach_type = tech('Антисептик', (225, 235, 220))
    lubricant_type = tech('Смазка', (205, 180, 55))
    enameled_glass = tech('Эмалевое стекло', (180, 230, 245))
    plasteel_ingot = tech('Пласталевый слиток', (150, 175, 190))
    hydrochloric_acid = tech('Соляная кислота', (125, 220, 145))
    polyaniline_type = tech('Полианилин', (95, 120, 200))
    synthetic_fibers = tech('Синтетические волокна', (165, 150, 190))
    aerogel_type = tech('Аэрогель', (130, 220, 235))
    benzene_type = tech('Бензол', (185, 80, 115))
    copper_wire = tech('Медный провод', (205, 120, 40))
    wiring_kit = tech('Комплект проводов', (180, 190, 205))
    advanced_wiring_kit = tech('Расширенный комплект проводов', (205, 170, 80))
    battery_type = tech('Батарея', (235, 210, 70))
    power_cell_type = tech('Энергоячейка', (80, 190, 225), 22)
    computer_chip = tech('Микросхема', (80, 185, 125))
    reactor_rod = tech('Топливный стержень реактора', (135, 215, 75), 22)
    ion_battery = tech('Ионная батарея', (80, 245, 150))
    ion_power_cell = tech('Ионная энергоячейка', (80, 245, 180), 22)
    fins_type = tech('Ласты', (90, 170, 190))
    standard_tank = tech('Обычный кислородный баллон', (175, 185, 195), 24)
    high_capacity_tank = tech('Кислородный баллон высокой ёмкости', (120, 170, 205), 24)
    lightweight_tank = tech('Лёгкий баллон высокой ёмкости', (115, 195, 205), 24)
    ultra_tank = tech('Баллон сверхвысокой ёмкости', (100, 150, 220), 24)
    rebreather_type = tech('Ребризер', (65, 115, 145))
    radiation_suit = tech('Противорадиационный костюм', (220, 170, 65), 24)
    reinforced_suit = tech('Укреплённый костюм ныряльщика', (70, 85, 100), 24)
    water_filtration_suit = tech('Фильтрационный костюм', (80, 140, 165), 24)
    compass_type = tech('Компас', (110, 185, 205))
    first_aid_kit = tech('Аптечка', (225, 230, 230))
    fire_extinguisher = tech('Огнетушитель', (220, 70, 50), 22)
    floating_air_pump = tech('Плавучий воздушный насос', (120, 180, 205))
    pipe_type = tech('Труба', (185, 195, 205))
    survival_knife = tech('Нож', (165, 175, 185), weapon=True, damage=10, weapon_range=1.7, weapon_type='нож')
    thermoblade = tech('Термоклинок', (245, 120, 50), weapon=True, damage=20, weapon_range=1.7, weapon_type='нож')
    scanner_type = tech('Сканер', (80, 175, 215))
    repair_tool = tech('Ремонтный инструмент', (85, 165, 205))
    repair_kit = tech('Ремкомплект', (235, 155, 70), 22)
    flashlight_type = tech('Фонарик', (220, 220, 180))
    habitat_builder_type = tech('Строитель', (95, 190, 205))
    air_bladder = tech('Воздушный пузырь', (230, 175, 70))
    pathfinder_tool = tech('Инструмент исследователя', (100, 205, 175))
    propulsion_cannon = tech('Пропульсионная пушка', (110, 145, 205), 24, True, 4, 7, 'импульс')
    repulsion_cannon = tech('Репульсионная пушка', (150, 110, 210), 24, True, 8, 8, 'импульс')
    stasis_rifle = tech('Стазис-винтовка', (75, 145, 220), 28, True, 1, 9, 'стазис')
    laser_cutter = tech('Лазерный резак', (225, 110, 70), 24, True, 8, 2.5, 'лазер')
    flare_type = tech('Сигнальная ракета', (245, 90, 70))
    beacon_type = buildable('Радиомаяк', (235, 185, 65), size=24)
    creature_decoy = tech('Приманка для существ', (190, 110, 85))
    seaglide_type = tech('Морской глайдер', (70, 150, 190), 28)
    mobile_vehicle_bay = buildable('Переносной сборщик транспорта', (95, 150, 175), size=34)
    grav_trap = buildable('Грави-ловушка', (115, 120, 205), size=26)
    waterproof_locker = buildable('Водостойкий контейнер', (150, 165, 170), size=26)
    seamoth_type = buildable('Мотылёк', (220, 180, 70), size=42)
    prawn_suit_type = buildable('Костюм КРАБ', (205, 115, 65), size=44)
    cyclops_type = buildable('Циклоп', (85, 130, 155), size=58)
    seamoth_type.image = generate_detailed_seamoth_vehicle_sprite(86)
    cyclops_type.image = generate_detailed_cyclops_vehicle_sprite(180)
    neptune_platform = buildable('Пусковая платформа «Нептуна»', (120, 135, 145), size=48)
    neptune_gantry = buildable('Башня подъёмника «Нептуна»', (135, 140, 150), size=48)
    neptune_boosters = buildable('Ускорители «Нептуна»', (180, 115, 65), size=48)
    neptune_fuel = buildable('Топливные баки «Нептуна»', (165, 95, 70), size=48)
    neptune_cockpit = buildable('Кабина «Нептуна»', (95, 155, 180), size=48)
    for _deployable in (beacon_type, mobile_vehicle_bay, grav_trap, waterproof_locker, seamoth_type, prawn_suit_type, cyclops_type, neptune_platform, neptune_gantry, neptune_boosters, neptune_fuel, neptune_cockpit):
        _deployable.requires_habitat_builder = False
    purple_tablet = tech('Фиолетовая скрижаль', (165, 90, 220))
    orange_tablet = tech('Оранжевая скрижаль', (235, 145, 55))
    blue_tablet = tech('Синяя скрижаль', (70, 145, 235))
    multipurpose_room = buildable('Многоцелевая комната', (95, 145, 160), size=38)
    hatch_type = buildable('Люк', (130, 165, 175))
    window_type = buildable('Окно', (125, 195, 215))
    foundation_type = buildable('Фундамент', (115, 125, 130), size=36)
    i_compartment = buildable('Прямой отсек', (100, 145, 155), size=34)
    l_compartment = buildable('Угловой отсек', (100, 145, 155), size=34)
    t_compartment = buildable('T-образный отсек', (100, 145, 155), size=34)
    x_compartment = buildable('Крестообразный отсек', (100, 145, 155), size=34)
    reinforcement_type = buildable('Усиление корпуса', (95, 105, 115), wall=True)
    glass_i_compartment = buildable('Прямой стеклянный отсек', (130, 205, 225), size=34)
    glass_l_compartment = buildable('Угловой стеклянный отсек', (130, 205, 225), size=34)
    observatory_type = buildable('Обсерватория', (120, 205, 225), size=38)
    vertical_connector = buildable('Вертикальный соединитель', (105, 150, 160), size=32)
    moonpool_type = buildable('Стыковочная шахта', (75, 135, 160), size=42)
    scanner_room_type = buildable('Комната сканирования', (85, 155, 170), size=40)
    large_room_type = buildable('Большая комната', (90, 135, 150), size=46)
    multipurpose_dome = buildable('Стеклянный купол многоцелевой комнаты', (145, 215, 230), size=38)
    large_room_dome = buildable('Стеклянный купол большой комнаты', (145, 215, 230), size=44)
    spotlight_type = buildable('Прожектор', (235, 230, 160), size=25)
    solar_panel_type = buildable('Солнечная панель', (65, 110, 180), size=30)
    power_transmitter = buildable('Энергопередатчик', (110, 175, 195), size=24)
    thermal_plant = buildable('Теплоэлектростанция', (180, 105, 70), size=30)
    exterior_growbed = buildable('Наружная грядка', (105, 125, 80), size=32)
    floodlight_type = buildable('Осветительная мачта', (230, 225, 170), size=26)
    base_air_pump = buildable('Стационарный воздушный насос', (120, 175, 195), size=26)
    ladder_type = buildable('Лестница', (150, 155, 160), size=25)
    bulkhead_type = buildable('Переборка', (125, 135, 140), wall=True)
    vehicle_upgrade_console = buildable('Консоль улучшения транспорта', (75, 145, 185), size=28)
    water_filtration_machine = buildable('Водоочистная станция', (110, 165, 185), size=30)
    alien_containment = buildable('Большой аквариум', (100, 175, 195), size=34)
    bioreactor = buildable('Биореактор', (100, 165, 95), size=32)
    nuclear_reactor = buildable('Ядерный реактор', (145, 165, 80), size=34)
    large_partition_door = buildable('Дверь переборки большой комнаты', (125, 135, 145), wall=True)
    large_partition = buildable('Переборка большой комнаты', (115, 125, 135), wall=True)
    modification_station = buildable('Модификационная станция', (100, 150, 195), size=30)
    fabricator_type = buildable('Изготовитель', (95, 165, 190), size=28)
    aquarium_type = buildable('Аквариум', (95, 175, 200), size=28)
    locker_type = buildable('Шкаф', (125, 145, 155), size=28)
    wall_locker = buildable('Настенный шкаф', (135, 150, 160), wall=True, size=26)
    plant_pot = buildable('Горшок для растений', (130, 100, 75), size=24)
    indoor_growbed = buildable('Внутренняя грядка', (120, 105, 75), size=30)
    plant_shelf = buildable('Стеллаж для растений', (130, 115, 85), size=26)
    radio_type = buildable('Радио', (95, 135, 170), size=24)
    composite_pot = buildable('Композитный горшок', (115, 95, 80), size=24)
    chic_pot = buildable('Роскошный горшок', (145, 115, 95), size=24)
    medical_fabricator = buildable('Изготовитель аптечек', (210, 220, 220), size=26)
    battery_charger = buildable('Зарядное устройство батарей', (100, 160, 185), size=28)
    power_cell_charger = buildable('Зарядное устройство энергоячеек', (90, 150, 180), size=30)
    centrifuge = buildable('Центрифуга', (120, 150, 165), size=28)
    bench_type = buildable('Скамья', (130, 115, 100), size=28)
    picture_frame = buildable('Фоторамка', (100, 110, 120), wall=True, size=22)
    wall_shelf = buildable('Настенная полка', (120, 120, 125), wall=True, size=24)
    bed_type = buildable('Кровать', (145, 125, 115), size=30)
    desk_type = buildable('Рабочий стол', (115, 120, 125), size=30)
    chair_type = buildable('Офисный стул', (95, 100, 105), size=24)
    vending_machine = buildable('Торговый автомат', (130, 145, 155), size=28)
    coffee_machine = buildable('Кофейный торговый автомат', (120, 105, 95), size=26)
    trash_can = buildable('Урна', (100, 110, 115), size=24)
    nuclear_waste = buildable('Утилизатор ядерных отходов', (145, 155, 75), size=26)
    seamoth_depth1 = tech('Модуль глубины «Мотылька» – Мод. 1')
    seamoth_depth2 = tech('Модуль глубины «Мотылька» – Мод. 2')
    seamoth_depth3 = tech('Модуль глубины «Мотылька» – Мод. 3')
    seamoth_solar = tech('Солнечная зарядка «Мотылька»')
    vehicle_storage = tech('Модуль хранилища')
    seamoth_perimeter = tech('Система периметральной защиты «Мотылька»')
    hull_reinforcement = tech('Усиление корпуса транспорта')
    seamoth_torpedo = tech('Торпедная установка «Мотылька»')
    seamoth_sonar = tech('Гидролокатор «Мотылька»')
    vortex_torpedo = tech('Вихревая торпеда')
    gas_torpedo = tech('Газовая торпеда')
    vehicle_engine_eff = tech('Модуль эффективности двигателя')
    prawn_depth1 = tech('Модуль глубины костюма КРАБ – Мод. 1')
    prawn_depth2 = tech('Модуль глубины костюма КРАБ – Мод. 2')
    prawn_jet = tech('Улучшение реактивного ранца костюма КРАБ')
    prawn_drill = tech('Бур костюма КРАБ')
    prawn_thermal = tech('Тепловой реактор костюма КРАБ')
    prawn_propulsion = tech('Пропульсионная пушка костюма КРАБ')
    prawn_grapple = tech('Рука-захват костюма КРАБ')
    prawn_torpedo = tech('Торпедная установка костюма КРАБ')
    scanner_hud = tech('Чип интерфейса комнаты сканирования')
    scanner_range = tech('Улучшение дальности сканирования')
    scanner_speed = tech('Улучшение скорости сканирования')
    camera_drone = tech('Дрон-камера')
    cyclops_engine_eff = tech('Модуль эффективности двигателя «Циклопа»')
    cyclops_depth1 = tech('Модуль глубины «Циклопа» – Мод. 1')
    cyclops_depth2 = tech('Модуль глубины «Циклопа» – Мод. 2')
    cyclops_depth3 = tech('Модуль глубины «Циклопа» – Мод. 3')
    cyclops_shield = tech('Генератор щита «Циклопа»')
    cyclops_sonar = tech('Гидролокатор «Циклопа»')
    cyclops_repair = tech('Ремонтный модуль стыковочного отсека «Циклопа»')
    cyclops_decoy = tech('Улучшение камеры приманок «Циклопа»')
    cyclops_fire = tech('Система пожаротушения «Циклопа»')
    cyclops_thermal = tech('Модуль теплового реактора «Циклопа»')
    wall_type = ObjectType('Стена', (150, 75, 0), 30, 'rect', is_buildable=True, is_wall=True)
    wall_type.image = generate_building_sprite('Стена', 30, (150, 75, 0), wall=True)
    floor_type = ObjectType('Пол', (100, 150, 100), 30, 'rect', is_buildable=True, is_wall=False)
    floor_type.image = generate_building_sprite('Пол', 30, (100, 150, 100), wall=False)
    laser_pistol = ObjectType('Лазерный пистолет', (255, 50, 50), 22, 'rect', is_weapon=True, damage=8, weapon_range=5, weapon_type='лазер')
    laser_pistol.image = generate_weapon_sprite(14, (255, 50, 50), (120, 0, 0))
    underwater_rifle = ObjectType('Подводное ружьё', (50, 150, 255), 28, 'rect', is_weapon=True, damage=12, weapon_range=8, weapon_type='ружьё')
    underwater_rifle.image = generate_weapon_sprite(24, (50, 150, 255), (0, 50, 130))
    plasma_cannon = ObjectType('Плазменная пушка', (200, 50, 200), 26, 'rect', is_weapon=True, damage=20, weapon_range=4, weapon_type='плазма')
    plasma_cannon.image = generate_weapon_sprite(18, (200, 50, 200), (90, 0, 110))
    lang_cannon = ObjectType('Дальняя пушка', (10, 50, 200), 30, 'rect', is_weapon=True, damage=1, weapon_range=15, weapon_type='ружьё')
    lang_cannon.image = generate_weapon_sprite(34, (10, 50, 200), (0, 15, 90))
    schweer_cannon = ObjectType('Тяжёлая пушка', (238, 51, 32), 34, 'rect', is_weapon=True, damage=50, weapon_range=10, weapon_type='ружьё')
    schweer_cannon.image = generate_weapon_sprite(38, (238, 51, 32), (110, 15, 10))
    diamond_cutter = ObjectType('Алмазный резак', (150, 255, 255), 30, 'rect', is_weapon=True, damage=35, weapon_range=6, weapon_type='лазер')
    diamond_cutter.image = generate_weapon_sprite(22, (150, 255, 255), (0, 130, 150))
    flamethrower = ObjectType('Огнемёт', (255, 120, 20), 32, 'rect', is_weapon=True, damage=15, weapon_range=3, weapon_type='огонь')
    flamethrower.image = generate_weapon_sprite(30, (255, 120, 20), (120, 40, 5))
    golden_pistol = ObjectType('Золотой пистолет', (255, 215, 0), 24, 'rect', is_weapon=True, damage=14, weapon_range=7, weapon_type='лазер')
    golden_pistol.image = generate_weapon_sprite(16, (255, 215, 0), (140, 100, 0))
    fabricator_type.crafting_station_name = 'Изготовитель'
    modification_station.crafting_station_name = 'Модификационная станция'
    mobile_vehicle_bay.crafting_station_name = 'Переносной сборщик транспорта'
    vehicle_upgrade_console.crafting_station_name = 'Консоль улучшения транспорта'
    scanner_room_type.crafting_station_name = 'Комната сканирования'
    cyclops_type.crafting_station_name = 'Циклоп'
    F = 'Изготовитель'
    H = 'Строитель'
    M = 'Модификационная станция'
    V = 'Переносной сборщик транспорта'
    U = 'Консоль улучшения транспорта'
    S = 'Комната сканирования'
    C = 'Циклоп'
    recipes = [Recipe('Титан x4', {metal_salvage_type: 1}, titan_type, 4, F, 'Ресурсы'), Recipe('Титановый слиток', {titan_type: 10}, titanium_ingot, 1, F, 'Основные материалы'), Recipe('Сетчатое волокно', {creepvine_sample_type: 2}, fiber_mesh, 1, F, 'Основные материалы'), Recipe('Силиконовая резина x2', {creepvine_seed_type: 1}, silicone_rubber, 2, F, 'Основные материалы'), Recipe('Стекло', {quartz_type: 2}, glass_type, 1, F, 'Основные материалы'), Recipe('Антисептик', {salt_type: 1, coral_tube_type: 1}, bleach_type, 1, F, 'Основные материалы'), Recipe('Смазка', {creepvine_seed_type: 1}, lubricant_type, 1, F, 'Основные материалы'), Recipe('Эмалевое стекло', {glass_type: 1, quartz_type: 1}, enameled_glass, 1, F, 'Основные материалы'), Recipe('Пласталевый слиток', {titanium_ingot: 1, lithium_type: 2}, plasteel_ingot, 1, F, 'Основные материалы'), Recipe('Соляная кислота', {deep_shroom_item: 3, salt_type: 1}, hydrochloric_acid, 1, F, 'Доп. материалы'), Recipe('Бензол', {blood_oil_type: 3}, benzene_type, 1, F, 'Доп. материалы'), Recipe('Синтетические волокна', {benzene_type: 1, fiber_mesh: 1}, synthetic_fibers, 1, F, 'Доп. материалы'), Recipe('Аэрогель', {ruby_type: 1, gel_sack_type: 1}, aerogel_type, 1, F, 'Доп. материалы'), Recipe('Полианилин', {gold_type: 1, hydrochloric_acid: 1}, polyaniline_type, 1, F, 'Доп. материалы'), Recipe('Медный провод', {copper_type: 2}, copper_wire, 1, F, 'Электроника'), Recipe('Батарея', {acid_mushroom_item: 2, copper_type: 1}, battery_type, 1, F, 'Электроника'), Recipe('Энергоячейка', {battery_type: 2, silicone_rubber: 1}, power_cell_type, 1, F, 'Электроника'), Recipe('Микросхема', {table_coral_type: 2, gold_type: 1, copper_wire: 1}, computer_chip, 1, F, 'Электроника'), Recipe('Комплект проводов', {silver_type: 2}, wiring_kit, 1, F, 'Электроника'), Recipe('Расширенный комплект проводов', {wiring_kit: 1, gold_type: 2, computer_chip: 1}, advanced_wiring_kit, 1, F, 'Электроника'), Recipe('Топливный стержень реактора', {uraninite_type: 3, lead_type: 1, titan_type: 1, glass_type: 1}, reactor_rod, 1, F, 'Электроника'), Recipe('Ионная батарея', {ion_cube_type: 1, gold_type: 1, silver_type: 1}, ion_battery, 1, F, 'Электроника'), Recipe('Ионная энергоячейка', {ion_battery: 2, silicone_rubber: 1}, ion_power_cell, 1, F, 'Электроника'), Recipe('Ласты', {silicone_rubber: 2}, fins_type, 1, F, 'Снаряжение'), Recipe('Обычный кислородный баллон', {titan_type: 3}, standard_tank, 1, F, 'Снаряжение'), Recipe('Баллон высокой ёмкости', {standard_tank: 1, glass_type: 2, titan_type: 4, silver_type: 1}, high_capacity_tank, 1, F, 'Снаряжение'), Recipe('Ребризер', {wiring_kit: 1, fiber_mesh: 1}, rebreather_type, 1, F, 'Снаряжение'), Recipe('Противорадиационный костюм', {fiber_mesh: 2, lead_type: 2}, radiation_suit, 1, F, 'Снаряжение'), Recipe('Укреплённый костюм ныряльщика', {synthetic_fibers: 1, diamond_type: 2, titan_type: 2}, reinforced_suit, 1, F, 'Снаряжение'), Recipe('Фильтрационный костюм', {synthetic_fibers: 1, aerogel_type: 1}, water_filtration_suit, 1, F, 'Снаряжение'), Recipe('Компас', {copper_wire: 1, wiring_kit: 1}, compass_type, 1, F, 'Снаряжение'), Recipe('Аптечка', {fiber_mesh: 1}, first_aid_kit, 1, F, 'Снаряжение'), Recipe('Шприц с антибиотиком', {bleach_type: 1, gel_sack_type: 1, glass_type: 1}, antibiotic_syringe, 1, F, 'Снаряжение'), Recipe('Огнетушитель', {titan_type: 3}, fire_extinguisher, 1, F, 'Снаряжение'), Recipe('Плавучий воздушный насос', {titan_type: 2}, floating_air_pump, 1, F, 'Снаряжение'), Recipe('Труба', {titan_type: 2}, pipe_type, 5, F, 'Снаряжение'), Recipe('Нож', {silicone_rubber: 1, titan_type: 1}, survival_knife, 1, F, 'Инструменты'), Recipe('Сканер', {battery_type: 1, titan_type: 1}, scanner_type, 1, F, 'Инструменты'), Recipe('Ремонтный инструмент', {silicone_rubber: 1, cave_sulfur_type: 1, titan_type: 1}, repair_tool, 1, F, 'Инструменты'), Recipe('Ремкомплект', {titan_type: 2, silicone_rubber: 1, cave_sulfur_type: 1}, repair_kit, 1, F, 'Инструменты'), Recipe('Фонарик', {battery_type: 1, glass_type: 1}, flashlight_type, 1, F, 'Инструменты'), Recipe('Строитель', {computer_chip: 1, wiring_kit: 1, battery_type: 1}, habitat_builder_type, 1, F, 'Инструменты'), Recipe('Воздушный пузырь', {silicone_rubber: 1, gel_sack_type: 1, titan_type: 1}, air_bladder, 1, F, 'Инструменты'), Recipe('Инструмент исследователя', {creepvine_sample_type: 1, copper_wire: 1, titan_type: 1}, pathfinder_tool, 1, F, 'Инструменты'), Recipe('Пропульсионная пушка', {wiring_kit: 1, battery_type: 1, titan_type: 1}, propulsion_cannon, 1, F, 'Инструменты'), Recipe('Стазис-винтовка', {computer_chip: 1, battery_type: 1, magnetite_type: 2}, stasis_rifle, 1, F, 'Инструменты'), Recipe('Лазерный резак', {diamond_type: 2, battery_type: 1, titan_type: 1, cave_sulfur_type: 1}, laser_cutter, 1, F, 'Инструменты'), Recipe('Сигнальная ракета', {cave_sulfur_type: 1}, flare_type, 5, F, 'Инструменты'), Recipe('Радиомаяк', {copper_wire: 1, titan_type: 1}, beacon_type, 1, F, 'Размещаемое'), Recipe('Приманка для существ', {titan_type: 3, wiring_kit: 1}, creature_decoy, 1, F, 'Размещаемое'), Recipe('Морской глайдер', {battery_type: 1, lubricant_type: 1, copper_wire: 1, titan_type: 1}, seaglide_type, 1, F, 'Размещаемое'), Recipe('Переносной сборщик транспорта', {titanium_ingot: 1, lubricant_type: 1, power_cell_type: 1}, mobile_vehicle_bay, 1, F, 'Размещаемое'), Recipe('Грави-ловушка', {battery_type: 1, copper_wire: 1, titan_type: 1}, grav_trap, 1, F, 'Размещаемое'), Recipe('Водостойкий контейнер', {titan_type: 4}, waterproof_locker, 1, F, 'Размещаемое'), Recipe('Фиолетовая скрижаль', {ion_cube_type: 1, diamond_type: 2}, purple_tablet, 1, F, 'Предтечи'), Recipe('Оранжевая скрижаль', {ion_cube_type: 1, nickel_type: 2}, orange_tablet, 1, F, 'Предтечи'), Recipe('Синяя скрижаль', {ion_cube_type: 1, kyanite_type: 2}, blue_tablet, 1, F, 'Предтечи'), Recipe('Термоклинок', {survival_knife: 1, battery_type: 1}, thermoblade, 1, M, 'Модификации'), Recipe('Лёгкий баллон высокой ёмкости', {high_capacity_tank: 1, plasteel_ingot: 1}, lightweight_tank, 1, M, 'Модификации'), Recipe('Баллон сверхвысокой ёмкости', {high_capacity_tank: 1, lithium_type: 4}, ultra_tank, 1, M, 'Модификации'), Recipe('Репульсионная пушка', {propulsion_cannon: 1, computer_chip: 1, magnetite_type: 2}, repulsion_cannon, 1, M, 'Модификации'), Recipe('Модуль глубины «Мотылька» – Мод. 2', {seamoth_depth1: 1, plasteel_ingot: 1, magnetite_type: 2, enameled_glass: 1}, seamoth_depth2, 1, M, 'Модификации'), Recipe('Модуль глубины «Мотылька» – Мод. 3', {seamoth_depth2: 1, plasteel_ingot: 1, ruby_type: 3}, seamoth_depth3, 1, M, 'Модификации'), Recipe('Модуль глубины костюма КРАБ – Мод. 2', {prawn_depth1: 1, titan_type: 5, lithium_type: 2, kyanite_type: 3}, prawn_depth2, 1, M, 'Модификации'), Recipe('Модуль глубины «Циклопа» – Мод. 2', {cyclops_depth1: 1, plasteel_ingot: 1, nickel_type: 3}, cyclops_depth2, 1, M, 'Модификации'), Recipe('Модуль глубины «Циклопа» – Мод. 3', {cyclops_depth2: 1, plasteel_ingot: 1, kyanite_type: 3}, cyclops_depth3, 1, M, 'Модификации'), Recipe('Мотылёк', {titanium_ingot: 1, power_cell_type: 1, glass_type: 2, lubricant_type: 1, lead_type: 1}, seamoth_type, 1, V, 'Транспорт'), Recipe('Костюм КРАБ', {plasteel_ingot: 2, aerogel_type: 2, enameled_glass: 1, diamond_type: 2, lead_type: 2}, prawn_suit_type, 1, V, 'Транспорт'), Recipe('Циклоп', {plasteel_ingot: 3, enameled_glass: 3, lubricant_type: 1, advanced_wiring_kit: 1, lead_type: 3}, cyclops_type, 1, V, 'Транспорт'), Recipe('Пусковая платформа «Нептуна»', {titanium_ingot: 2, computer_chip: 1, lead_type: 4}, neptune_platform, 1, V, 'Нептун'), Recipe('Башня подъёмника «Нептуна»', {plasteel_ingot: 1, copper_wire: 1, lubricant_type: 1}, neptune_gantry, 1, V, 'Нептун'), Recipe('Ускорители «Нептуна»', {plasteel_ingot: 1, nickel_type: 3, wiring_kit: 1, aerogel_type: 1}, neptune_boosters, 1, V, 'Нептун'), Recipe('Топливные баки «Нептуна»', {plasteel_ingot: 1, ion_power_cell: 4, kyanite_type: 4, cave_sulfur_type: 2}, neptune_fuel, 1, V, 'Нептун'), Recipe('Кабина «Нептуна»', {cyclops_shield: 1, plasteel_ingot: 1, enameled_glass: 1, computer_chip: 1}, neptune_cockpit, 1, V, 'Нептун'), Recipe('Модуль эффективности двигателя', {computer_chip: 1, polyaniline_type: 1}, vehicle_engine_eff, 1, U, 'Улучшения транспорта'), Recipe('Солнечная зарядка «Мотылька»', {advanced_wiring_kit: 1, enameled_glass: 1}, seamoth_solar, 1, U, 'Улучшения транспорта'), Recipe('Модуль хранилища', {titan_type: 3, lithium_type: 1}, vehicle_storage, 1, U, 'Улучшения транспорта'), Recipe('Система периметральной защиты «Мотылька»', {polyaniline_type: 1, wiring_kit: 1}, seamoth_perimeter, 1, U, 'Улучшения транспорта'), Recipe('Усиление корпуса транспорта', {titan_type: 3, lithium_type: 1, diamond_type: 1}, hull_reinforcement, 1, U, 'Улучшения транспорта'), Recipe('Торпедная установка «Мотылька»', {titan_type: 3, lithium_type: 1, aerogel_type: 1}, seamoth_torpedo, 1, U, 'Улучшения транспорта'), Recipe('Гидролокатор «Мотылька»', {copper_wire: 1, magnetite_type: 2}, seamoth_sonar, 1, U, 'Улучшения транспорта'), Recipe('Вихревая торпеда', {titan_type: 1, magnetite_type: 1}, vortex_torpedo, 2, U, 'Боеприпасы'), Recipe('Модуль глубины «Мотылька» – Мод. 1', {titanium_ingot: 1, glass_type: 2}, seamoth_depth1, 1, U, 'Улучшения транспорта'), Recipe('Улучшение реактивного ранца костюма КРАБ', {nickel_type: 2, cave_sulfur_type: 1, titan_type: 1, lithium_type: 1}, prawn_jet, 1, U, 'КРАБ'), Recipe('Бур костюма КРАБ', {titan_type: 5, lithium_type: 1, diamond_type: 4}, prawn_drill, 1, U, 'КРАБ'), Recipe('Тепловой реактор костюма КРАБ', {kyanite_type: 2, polyaniline_type: 2, wiring_kit: 1}, prawn_thermal, 1, U, 'КРАБ'), Recipe('Пропульсионная пушка костюма КРАБ', {computer_chip: 1, titan_type: 5, lithium_type: 1, magnetite_type: 2}, prawn_propulsion, 1, U, 'КРАБ'), Recipe('Рука-захват костюма КРАБ', {advanced_wiring_kit: 1, titan_type: 5, lithium_type: 1, benzene_type: 1}, prawn_grapple, 1, U, 'КРАБ'), Recipe('Торпедная установка костюма КРАБ', {titan_type: 5, lithium_type: 1, aerogel_type: 1}, prawn_torpedo, 1, U, 'КРАБ'), Recipe('Модуль глубины костюма КРАБ – Мод. 1', {plasteel_ingot: 1, nickel_type: 3, ruby_type: 2}, prawn_depth1, 1, U, 'КРАБ'), Recipe('Чип интерфейса комнаты сканирования', {computer_chip: 1, magnetite_type: 1}, scanner_hud, 1, S, 'Комната сканирования'), Recipe('Улучшение дальности сканирования', {copper_type: 1, magnetite_type: 1}, scanner_range, 1, S, 'Комната сканирования'), Recipe('Улучшение скорости сканирования', {silver_type: 1, gold_type: 1}, scanner_speed, 1, S, 'Комната сканирования'), Recipe('Дрон-камера', {computer_chip: 1, battery_type: 1, glass_type: 1, titan_type: 1}, camera_drone, 1, S, 'Комната сканирования'), Recipe('Модуль эффективности двигателя «Циклопа»', {computer_chip: 1, benzene_type: 1, polyaniline_type: 1}, cyclops_engine_eff, 1, C, 'Циклоп'), Recipe('Модуль глубины «Циклопа» – Мод. 1', {plasteel_ingot: 1, ruby_type: 3}, cyclops_depth1, 1, C, 'Циклоп'), Recipe('Генератор щита «Циклопа»', {advanced_wiring_kit: 1, polyaniline_type: 1, power_cell_type: 1}, cyclops_shield, 1, C, 'Циклоп'), Recipe('Гидролокатор «Циклопа»', {computer_chip: 1, magnetite_type: 3}, cyclops_sonar, 1, C, 'Циклоп'), Recipe('Ремонтный модуль стыковочного отсека «Циклопа»', {repair_tool: 1, copper_wire: 1}, cyclops_repair, 1, C, 'Циклоп'), Recipe('Улучшение камеры приманок «Циклопа»', {titan_type: 3, lithium_type: 1}, cyclops_decoy, 1, C, 'Циклоп'), Recipe('Система пожаротушения «Циклопа»', {aerogel_type: 2, cave_sulfur_type: 2}, cyclops_fire, 1, C, 'Циклоп'), Recipe('Модуль теплового реактора «Циклопа»', {kyanite_type: 4, polyaniline_type: 2, wiring_kit: 1}, cyclops_thermal, 1, C, 'Циклоп'), Recipe('Многоцелевая комната', {titan_type: 6}, multipurpose_room, 1, H, 'Детали базы'), Recipe('Люк', {quartz_type: 1, titan_type: 2}, hatch_type, 1, H, 'Детали базы'), Recipe('Окно', {glass_type: 1}, window_type, 1, H, 'Детали базы'), Recipe('Фундамент', {titan_type: 2, lead_type: 2}, foundation_type, 1, H, 'Детали базы'), Recipe('Прямой отсек', {titan_type: 2}, i_compartment, 1, H, 'Детали базы'), Recipe('Угловой отсек', {titan_type: 2}, l_compartment, 1, H, 'Детали базы'), Recipe('T-образный отсек', {titan_type: 3}, t_compartment, 1, H, 'Детали базы'), Recipe('Крестообразный отсек', {titan_type: 3}, x_compartment, 1, H, 'Детали базы'), Recipe('Усиление корпуса', {lithium_type: 1, titan_type: 3}, reinforcement_type, 1, H, 'Детали базы'), Recipe('Прямой стеклянный отсек', {glass_type: 2}, glass_i_compartment, 1, H, 'Детали базы'), Recipe('Угловой стеклянный отсек', {glass_type: 2}, glass_l_compartment, 1, H, 'Детали базы'), Recipe('Обсерватория', {enameled_glass: 2, titan_type: 1}, observatory_type, 1, H, 'Детали базы'), Recipe('Вертикальный соединитель', {titan_type: 2}, vertical_connector, 1, H, 'Детали базы'), Recipe('Стыковочная шахта', {titanium_ingot: 2, lubricant_type: 1, lead_type: 2}, moonpool_type, 1, H, 'Детали базы'), Recipe('Комната сканирования', {titan_type: 5, copper_type: 2, gold_type: 1, table_coral_type: 1}, scanner_room_type, 1, H, 'Детали базы'), Recipe('Большая комната', {plasteel_ingot: 2}, large_room_type, 1, H, 'Детали базы'), Recipe('Стеклянный купол многоцелевой комнаты', {enameled_glass: 2, titan_type: 2, lithium_type: 1}, multipurpose_dome, 1, H, 'Детали базы'), Recipe('Стеклянный купол большой комнаты', {enameled_glass: 4, titan_type: 4, lithium_type: 2}, large_room_dome, 1, H, 'Детали базы'), Recipe('Прожектор', {glass_type: 1, titan_type: 2}, spotlight_type, 1, H, 'Внешние модули'), Recipe('Солнечная панель', {quartz_type: 2, titan_type: 2, copper_type: 1}, solar_panel_type, 1, H, 'Внешние модули'), Recipe('Энергопередатчик', {gold_type: 1, titan_type: 1}, power_transmitter, 1, H, 'Внешние модули'), Recipe('Теплоэлектростанция', {titan_type: 5, magnetite_type: 2, aerogel_type: 1}, thermal_plant, 1, H, 'Внешние модули'), Recipe('Наружная грядка', {titan_type: 2}, exterior_growbed, 1, H, 'Внешние модули'), Recipe('Осветительная мачта', {glass_type: 1, titan_type: 1}, floodlight_type, 1, H, 'Внешние модули'), Recipe('Стационарный воздушный насос', {titan_type: 2}, base_air_pump, 1, H, 'Внешние модули'), Recipe('Лестница', {titan_type: 2}, ladder_type, 1, H, 'Внутренние элементы'), Recipe('Переборка', {titan_type: 3, silicone_rubber: 1}, bulkhead_type, 1, H, 'Внутренние элементы'), Recipe('Консоль улучшения транспорта', {titan_type: 3, computer_chip: 1, copper_wire: 1}, vehicle_upgrade_console, 1, H, 'Внутренние элементы'), Recipe('Водоочистная станция', {titan_type: 3, copper_wire: 1, aerogel_type: 1}, water_filtration_machine, 1, H, 'Внутренние элементы'), Recipe('Большой аквариум', {glass_type: 5, titan_type: 2}, alien_containment, 1, H, 'Внутренние элементы'), Recipe('Биореактор', {titan_type: 3, wiring_kit: 1, lubricant_type: 1}, bioreactor, 1, H, 'Внутренние элементы'), Recipe('Ядерный реактор', {plasteel_ingot: 3, advanced_wiring_kit: 1, lead_type: 3}, nuclear_reactor, 1, H, 'Внутренние элементы'), Recipe('Дверь переборки большой комнаты', {titan_type: 1}, large_partition_door, 1, H, 'Внутренние элементы'), Recipe('Переборка большой комнаты', {titan_type: 1}, large_partition, 1, H, 'Внутренние элементы'), Recipe('Модификационная станция', {computer_chip: 1, titan_type: 1, diamond_type: 1, lead_type: 1}, modification_station, 1, H, 'Внутренние модули'), Recipe('Изготовитель', {titan_type: 1, gold_type: 1, table_coral_type: 1}, fabricator_type, 1, H, 'Внутренние модули'), Recipe('Аквариум', {glass_type: 2, titan_type: 1}, aquarium_type, 1, H, 'Внутренние модули'), Recipe('Шкаф', {quartz_type: 1, titan_type: 2}, locker_type, 1, H, 'Внутренние модули'), Recipe('Настенный шкаф', {titan_type: 2}, wall_locker, 1, H, 'Внутренние модули'), Recipe('Горшок для растений', {titan_type: 2}, plant_pot, 1, H, 'Внутренние модули'), Recipe('Внутренняя грядка', {titan_type: 2}, indoor_growbed, 1, H, 'Внутренние модули'), Recipe('Стеллаж для растений', {titan_type: 1}, plant_shelf, 1, H, 'Внутренние модули'), Recipe('Радио', {titan_type: 1, copper_type: 1}, radio_type, 1, H, 'Внутренние модули'), Recipe('Композитный горшок', {titan_type: 2}, composite_pot, 1, H, 'Внутренние модули'), Recipe('Роскошный горшок', {titan_type: 2}, chic_pot, 1, H, 'Внутренние модули'), Recipe('Изготовитель аптечек', {computer_chip: 1, fiber_mesh: 1, silver_type: 1, titan_type: 1}, medical_fabricator, 1, H, 'Внутренние модули'), Recipe('Зарядное устройство батарей', {wiring_kit: 1, copper_wire: 1, titan_type: 1}, battery_charger, 1, H, 'Внутренние модули'), Recipe('Зарядное устройство энергоячеек', {advanced_wiring_kit: 1, ruby_type: 2, titan_type: 2}, power_cell_charger, 1, H, 'Внутренние модули'), Recipe('Центрифуга', {titan_type: 2, wiring_kit: 1}, centrifuge, 1, H, 'Внутренние модули'), Recipe('Скамья', {titan_type: 2}, bench_type, 1, H, 'Мебель'), Recipe('Фоторамка', {copper_wire: 1}, picture_frame, 1, H, 'Мебель'), Recipe('Настенная полка', {titan_type: 1}, wall_shelf, 1, H, 'Мебель'), Recipe('Кровать', {titan_type: 2, fiber_mesh: 1}, bed_type, 1, H, 'Мебель'), Recipe('Рабочий стол', {titan_type: 1}, desk_type, 1, H, 'Мебель'), Recipe('Офисный стул', {titan_type: 1}, chair_type, 1, H, 'Мебель'), Recipe('Торговый автомат', {glass_type: 1, titan_type: 2}, vending_machine, 1, H, 'Мебель'), Recipe('Кофейный торговый автомат', {titan_type: 2}, coffee_machine, 1, H, 'Мебель'), Recipe('Урна', {titan_type: 2}, trash_can, 1, H, 'Мебель'), Recipe('Утилизатор ядерных отходов', {titan_type: 2}, nuclear_waste, 1, H, 'Мебель'), Recipe('2D-стена', {rock_type: 2, limestone_type: 2}, wall_type, 1, F, '2D-совместимость'), Recipe('2D-пол', {limestone_type: 1, sand_type: 2, clay_type: 1}, floor_type, 1, F, '2D-совместимость')]
    biomes = []
    AURORA_HULL = (78, 72, 62)
    AURORA_WALL = (102, 94, 82)
    AURORA_NOSE = (60, 55, 46)
    AURORA_ENGINE = (58, 48, 42)
    AURORA_REACTOR = (72, 48, 54)
    AURORA_FLOOR = (86, 80, 70)
    AURORA_ADMIN = (70, 82, 92)
    AURORA_CARGO = (93, 78, 58)
    AURORA_DOCK = (60, 78, 92)
    AURORA_LIVING = (92, 72, 67)
    AURORA_LAB = (66, 78, 82)
    AURORA_BLACKBOX = (50, 58, 68)
    AURORA_SHIFT_X = 550

    def aurora_rect(rect):
        x1, y1, x2, y2 = rect
        return (x1 + AURORA_SHIFT_X, y1, x2 + AURORA_SHIFT_X, y2)

    def add_aurora_floor(name, rect, color=AURORA_FLOOR, objects=None, priority=120):
        if objects is None:
            objects = []
        biome = Biome(name, color, *aurora_rect(rect), list(objects), [], passable=True, cave_enabled=False, priority=priority)
        biome.aurora_style = True
        biome.location_kind = 'aurora_room'
        biomes.append(biome)
        return biome

    def add_aurora_wall(name, rect, color=AURORA_WALL, priority=170):
        biome = Biome(name, color, *aurora_rect(rect), [], [], passable=False, cave_enabled=False, priority=priority)
        biome.aurora_style = True
        biome.location_kind = 'aurora_wall'
        biomes.append(biome)
        return biome

    def add_aurora_passage(name, rect, color=AURORA_FLOOR, priority=185):
        return add_aurora_floor(name, rect, color=color, objects=[], priority=priority)
    add_aurora_floor('Внутренние отсеки Авроры', (196, 381, 299, 649), AURORA_FLOOR, priority=100)
    for rect in [(247, 351, 253, 356), (241, 357, 259, 362), (234, 363, 266, 368), (227, 369, 273, 374), (221, 375, 279, 380)]:
        add_aurora_floor('Разорванная носовая часть Авроры', rect, AURORA_FLOOR, priority=105)
    AURORA_ROOMS = [('Входной коридор Авроры / Airlock', (238, 370, 260, 392), AURORA_ADMIN), ('Коридор администрации Авроры', (214, 390, 260, 404), AURORA_ADMIN), ('Кабинет администрации Авроры', (200, 402, 234, 426), AURORA_ADMIN), ('Обрушенный коридор Авроры к грузовому отсеку', (238, 402, 263, 434), AURORA_HULL), ('Коридор грузового отсека Авроры', (235, 426, 265, 448), AURORA_CARGO), ('Главный грузовой отсек Авроры', (202, 444, 292, 500), AURORA_CARGO), ('Грузовой подъёмник Авроры', (260, 486, 287, 511), AURORA_CARGO), ('Нижний грузовой отсек Авроры', (218, 498, 288, 522), AURORA_CARGO), ('Четырёхсторонний коридор Авроры', (234, 516, 266, 541), AURORA_FLOOR), ('Отсек «Мотылёк» Авроры', (197, 518, 232, 561), AURORA_DOCK), ('Коридор двигательного отсека Авроры', (263, 518, 282, 551), AURORA_ENGINE), ('Двигательный отсек / реактор Авроры', (273, 538, 298, 586), AURORA_REACTOR), ('Раздевалка Авроры', (232, 538, 263, 563), AURORA_LIVING), ('Отсек костюмов КРАБ / PRAWN Bay Авроры', (197, 562, 272, 611), AURORA_DOCK), ('Пролом корпуса PRAWN Bay Авроры', (270, 582, 293, 610), AURORA_HULL), ('Жилой коридор Авроры', (198, 607, 270, 646), AURORA_LIVING), ('Столовая Авроры', (199, 611, 220, 632), AURORA_LIVING), ('Каюта №1 Авроры', (222, 611, 235, 625), AURORA_LIVING), ('Каюта капитана Авроры', (237, 611, 254, 625), AURORA_LIVING), ('Каюта №3 Авроры', (256, 611, 269, 625), AURORA_LIVING), ('Каюта №4 Авроры', (222, 628, 235, 643), AURORA_LIVING), ('Каюта №6 Авроры', (237, 628, 249, 643), AURORA_LIVING), ('Каюта №7 Авроры', (251, 628, 265, 643), AURORA_LIVING), ('Подводный технический проход Авроры', (266, 604, 282, 636), AURORA_DOCK), ('Верхняя комната чёрного ящика Авроры', (270, 618, 292, 634), AURORA_BLACKBOX), ('Нижняя комната чёрного ящика Авроры', (270, 634, 292, 648), AURORA_BLACKBOX), ('Большая катушка данных Авроры', (258, 634, 274, 648), AURORA_BLACKBOX), ('Лаборатория Авроры', (280, 603, 299, 635), AURORA_LAB), ('Обрушенный коридор лаборатории Авроры', (286, 584, 299, 605), AURORA_HULL), ('Выходной коридор Авроры', (291, 570, 299, 594), AURORA_FLOOR)]
    for room_name, room_rect, room_color in AURORA_ROOMS:
        # Находки размещаются отдельно по клеткам, а не случайно по площади.
        # Уникальные пушки вернутся в свои отсеки вместе с остальным грузом.
        aurora_room = add_aurora_floor(room_name, room_rect, room_color, objects=[], priority=125)
        aurora_room.aurora_loot_room = True
    # Внутренние переборки: каждое помещение обрамлено металлической стеной.
    # Двери образуются приоритетными проходимыми накладками.
    # Все координаты ниже локальные (до смещения корпуса на восток).
    for room_index, (room_name, room_rect, room_color) in enumerate(AURORA_ROOMS):
        x1, y1, x2, y2 = room_rect
        if x2 - x1 < 5 or y2 - y1 < 5:
            continue
        for edge in ((x1, y1, x2, y1), (x1, y2, x2, y2),
                     (x1, y1 + 1, x1, y2 - 1), (x2, y1 + 1, x2, y2 - 1)):
            add_aurora_wall('Переборка: ' + room_name, edge, priority=158)
        # Широкие дверные проёмы с каждой стороны: проходы соединяют
        # помещения между собой и с общими коридорами.
        mid_x = (x1 + x2) // 2
        mid_y = (y1 + y2) // 2
        for edge, passage in (
            ('север', (mid_x - 1, y1, mid_x + 1, y1)),
            ('юг', (mid_x - 1, y2, mid_x + 1, y2)),
            ('запад', (x1, mid_y - 1, x1, mid_y + 1)),
            ('восток', (x2, mid_y - 1, x2, mid_y + 1))):
            px1, py1, px2, py2 = passage
            # Не открываем дверные проёмы за пределы основной Авроры.
            if px1 <= 196 or px2 >= 299 or py2 >= 649:
                continue
            add_aurora_passage('Дверь (' + edge + '): ' + room_name,
                               passage, color=room_color, priority=165)
    for rect in [(195, 380, 195, 650), (300, 380, 300, 569), (300, 606, 300, 650), (195, 650, 300, 650), (195, 380, 220, 380), (280, 380, 300, 380)]:
        add_aurora_wall('Корпус Авроры', rect, AURORA_HULL)
    for rect in [(220, 375, 226, 375), (226, 369, 233, 369), (233, 363, 240, 363), (240, 357, 246, 357), (254, 357, 260, 357), (260, 363, 267, 363), (267, 369, 274, 369), (274, 375, 280, 375)]:
        add_aurora_wall('Нос Авроры', rect, AURORA_NOSE)
    for rect in [(196, 438, 299, 439), (196, 513, 299, 514), (196, 560, 299, 561), (196, 605, 299, 606)]:
        add_aurora_wall('Внутренняя переборка Авроры', rect)
    for name, rect, color in [('Верхний вход Авроры', (244, 350, 256, 382), AURORA_FLOOR), ('Дверь грузового отсека Авроры', (245, 437, 256, 441), AURORA_CARGO), ('Проход к нижнему грузовому отсеку Авроры', (245, 512, 258, 516), AURORA_CARGO), ('Проход к PRAWN Bay Авроры', (238, 559, 258, 563), AURORA_DOCK), ('Проход в жилые отсеки Авроры', (204, 604, 218, 608), AURORA_LIVING), ('Проход к чёрному ящику Авроры', (264, 604, 278, 608), AURORA_BLACKBOX), ('Нижний аварийный вход Авроры', (296, 588, 305, 602), AURORA_LAB)]:
        add_aurora_passage(name, rect, color=color)

    def _ring_xy(radius, angle):
        return (500 + math.cos(angle) * radius, 500 + math.sin(angle) * radius)

    def _ring_disk(radius, seed=0):
        return [_ring_xy(radius * (1.0 + 0.012 * math.sin(j * 3.1 + seed)), j * math.tau / 72) for j in range(72)]

    def _ring_sector(inner_radius, outer_radius, a0, a1, seed):
        a0 -= 0.009
        a1 += 0.009
        n = max(7, int((a1 - a0) * 55))
        outer = []
        inner = []
        for j in range(n + 1):
            angle = a0 + (a1 - a0) * j / n
            wave = 3.0 * math.sin(6 * angle + seed * 0.31) + 2.5 * math.sin(13 * angle + seed * 0.17)
            outer.append(_ring_xy(outer_radius + wave, angle))
            inner.append(_ring_xy(max(1, inner_radius + wave * 0.45), angle))
        return outer + list(reversed(inner))

    def _ring_add(name, color, poly, resources=(), fauna=(), cave=False, priority=10, label=''):
        b = Biome(name, color, 0, 0, 0, 0, list(resources), list(fauna), passable=True, polygon=poly, map_label=label, cave_enabled=cave, priority=priority)
        depth_ranges = {'Мелководье': '0–100 м', 'Глубоководье': '100–500 м', 'Центральное море': '300–1000+ м', 'Побережье': 'суша', 'Острова': 'суша', 'Коралловый риф': '0–300 м', 'Кельповый лес': '0–100 м', 'Био-люминесцентная зона': '300–1000 м', 'Озёра (внешние)': '0–100 м', 'Реки': '0–100 м', 'Притоки': '0–100 м', 'Водопады': '0–100 м'}
        b.depth_label = 'суша' if name in LAND_BIOMES else depth_ranges.get(name)
        biomes.append(b)
        return b
    biomes[:] = [b for b in biomes if getattr(b, 'aurora_style', False)]
    _RING_RES = {'Мелководье': [(titan_type, 3), (quartz_type, 4), (sand_type, 3), (salt_type, 2)], 'Глубоководье': [(quartz_type, 4), (silver_type, 2), (lithium_type, 2), (lead_type, 2)], 'Центральное море': [(quartz_type, 2), (gold_type, 1), (iron_type, 1)], 'Побережье': [(sand_type, 6), (quartz_type, 3), (titan_type, 2), (limestone_type, 3)], 'Острова': [(rock_type, 4), (iron_type, 3), (silver_type, 2), (lithium_type, 2)], 'Коралловый риф': [(table_coral_type, 4), (coral_tube_type, 3), (quartz_type, 3), (ruby_type, 1)], 'Леса': [(rock_type, 4), (copper_type, 3), (limestone_type, 2), (clay_type, 2)], 'Кельповый лес': [(seaweed_type, 6), (creepvine_seed_type, 3), (salt_type, 2), (copper_type, 2)], 'Био-люминесцентная зона': [(quartz_type, 4), (ruby_type, 2), (nickel_type, 2), (gel_sack_type, 2)], 'Горы': [(rock_type, 6), (lithium_type, 4), (magnetite_type, 3), (diamond_type, 2)], 'Озёра (внешние)': [(quartz_type, 3), (salt_type, 3), (titan_type, 2)], 'Реки': [(quartz_type, 3), (salt_type, 2), (copper_type, 1)], 'Притоки': [(quartz_type, 1)], 'Водопады': [(quartz_type, 2), (silver_type, 1)]}
    _ring_add('Центральное море', (7, 22, 86), _ring_disk(222, 15), _RING_RES['Центральное море'], cave=True, priority=7, label='ЦЕНТРАЛЬНОЕ МОРЕ')
    SECTORS = 16
    for i in range(SECTORS):
        a0 = -math.pi + i * math.tau / SECTORS
        a1 = -math.pi + (i + 1) * math.tau / SECTORS
        _ring_add('Глубоководье', (4, 84 + i % 3 * 8, 161), _ring_sector(212, 272, a0, a1, 42), _RING_RES['Глубоководье'], cave=i == 6, priority=8)
        _ring_add('Мелководье', (15, 173 + i % 3 * 6, 213), _ring_sector(263, 311, a0, a1, 21), _RING_RES['Мелководье'], cave=i == 2, priority=10)
        _ring_add('Побережье', (213, 189 + i % 4 * 6, 122), _ring_sector(301, 337, a0, a1, 27), _RING_RES['Побережье'], cave=i == 10, priority=12)
        forest_color = (31 + 4 * (i % 4), 106 + 9 * (i % 5), 53 + 3 * (i % 3))
        _ring_add('Леса', forest_color, _ring_sector(326, 465, a0, a1, 34), _RING_RES['Леса'], cave=i in (3, 5, 12), priority=6)
    for j, deg in enumerate((-164, -96, -18, 34, 108, 161)):
        a = math.radians(deg)
        x, y = _ring_xy(283 if j % 2 else 312, a)
        _ring_add('Острова', (84, 105, 118), make_blob_points(x, y, 42, 31, 118 + j, point_count=22, jitter=0.15), _RING_RES['Острова'], cave=j == 1, priority=17)
    for j, deg in enumerate((-150, -103, -51, 8, 76, 136)):
        a = math.radians(deg)
        x, y = _ring_xy(235 + 9 * (j % 3), a)
        _ring_add('Коралловый риф', (143 + j % 2 * 15, 43, 167), make_blob_points(x, y, 32 + 3 * (j % 3), 22 + 3 * (j % 3), 280 + j, point_count=28, jitter=0.12), _RING_RES['Коралловый риф'], cave=j == 3, priority=19)
    for j, deg in enumerate((-170, -68, -9, 63, 151)):
        x, y = _ring_xy(285, math.radians(deg))
        _ring_add('Кельповый лес', (10, 110 + j * 5, 72), make_blob_points(x, y, 38, 29, 350 + j, point_count=26, jitter=0.14), _RING_RES['Кельповый лес'], cave=j == 1, priority=20)
    for j, deg in enumerate((-146, -83, -22, 30, 94, 168)):
        x, y = _ring_xy(219 + 8 * (j % 2), math.radians(deg))
        _ring_add('Био-люминесцентная зона', (58, 30, 170 + j * 8), make_blob_points(x, y, 31, 23, 440 + j, point_count=26, jitter=0.17), _RING_RES['Био-люминесцентная зона'], cave=j in (0, 4), priority=21)
    for j, deg in enumerate((-166, -111, -80, -40, 17, 63, 118, 155)):
        x, y = _ring_xy(395 + j % 3 * 8, math.radians(deg))
        _ring_add('Горы', (135 + j % 3 * 16, 143, 151), make_blob_points(x, y, 48 + 7 * (j % 3), 55 + j % 2 * 7, 510 + j, point_count=28, jitter=0.24), _RING_RES['Горы'], cave=j in (0, 2, 5), priority=23)
    for j, deg in enumerate((-155, -92, -29, 34, 92, 148)):
        a = math.radians(deg)
        lx, ly = _ring_xy(407 - 8 * (j % 2), a)
        _ring_add('Озёра (внешние)', (9, 108, 191), make_blob_points(lx, ly, 30 + j % 3 * 4, 24 + j % 3 * 4, 610 + j, point_count=26, jitter=0.17), _RING_RES['Озёра (внешние)'], priority=30)
        points = [_ring_xy(r, a + 0.055 * math.sin(r * 0.039 + j)) for r in (402, 378, 350, 326, 300, 278)]
        for q, (p1, p2) in enumerate(zip(points, points[1:])):
            _ring_add('Реки', (5, 145 + q * 7, 221), make_corridor_polygon(*p1, *p2, 12), _RING_RES['Реки'], priority=33)
        for sign in (-1, 1):
            tributary_angle = a + sign * 0.17
            p0 = _ring_xy(414, tributary_angle)
            p1 = _ring_xy(361, a + sign * 0.055)
            _ring_add('Притоки', (10, 110, 196), make_corridor_polygon(*p0, *p1, 6), _RING_RES['Притоки'], priority=34)
        wx, wy = points[-2]
        _ring_add('Водопады', (124, 231, 255), make_blob_points(wx, wy, 9, 13, 690 + j, point_count=14, jitter=0.07), _RING_RES['Водопады'], priority=36)
    for j, (x, y) in enumerate(((468, 505), (541, 493), (516, 542))):
        _ring_add('Острова', (100, 116, 105), make_blob_points(x, y, 15, 13, 750 + j, point_count=22, jitter=0.15), _RING_RES['Острова'], priority=24)
    extra_resource_profiles = {'Мелководье': [(acid_mushroom_item, 3), (table_coral_type, 2)], 'Глубоководье': [(nickel_type, 2), (gel_sack_type, 1)], 'Центральное море': [(ion_cube_type, 1)], 'Побережье': [(metal_salvage_type, 3), (sand_type, 3)], 'Острова': [(metal_salvage_type, 2), (nickel_type, 2)], 'Коралловый риф': [(table_coral_type, 3), (coral_tube_type, 4)], 'Леса': [(metal_salvage_type, 1), (clay_type, 2)], 'Кельповый лес': [(creepvine_sample_type, 4), (creepvine_seed_type, 3)], 'Био-люминесцентная зона': [(blood_oil_type, 2), (deep_shroom_item, 3), (ion_cube_type, 1)], 'Горы': [(nickel_type, 3), (kyanite_type, 1)], 'Озёра (внешние)': [(salt_type, 2)], 'Реки': [(copper_type, 2)]}
    for _biome in biomes:
        if _biome.cave_enabled:
            _biome.objects.extend(extra_resource_profiles.get(_biome.name, ()))
    SURFACE_FAUNA = {'Мелководье': [(zucchini_fish_type, 8)], 'Глубоководье': [(zucchini_fish_type, 7)], 'Центральное море': [(zucchini_fish_type, 16), (chbos_whale_type, 10)], 'Побережье': [], 'Острова': [], 'Коралловый риф': [(eggplant_fish_type, 12), (zucchini_fish_type, 4)], 'Леса': [], 'Кельповый лес': [(zucchini_fish_type, 5)], 'Био-люминесцентная зона': [(eggplant_fish_type, 14)], 'Горы': [], 'Озёра (внешние)': [(zucchini_fish_type, 4)], 'Реки': [(zucchini_fish_type, 5)], 'Притоки': [(zucchini_fish_type, 2)], 'Водопады': []}
    dense_fish_additions = {}
    small_fish_types = {zucchini_fish_type, eggplant_fish_type}
    weak_predator_types = set()
    for biome_name, fauna in list(SURFACE_FAUNA.items()):
        merged = OrderedDict()
        for animal_type, count in fauna:
            new_count = count
            if animal_type in small_fish_types:
                new_count = max(new_count + 2, int(round(new_count * 1.35)))
            elif animal_type in weak_predator_types:
                new_count = max(new_count + 1, int(round(new_count * 1.2)))
            merged[animal_type] = merged.get(animal_type, 0) + new_count
        for animal_type, add_count in dense_fish_additions.get(biome_name, []):
            merged[animal_type] = merged.get(animal_type, 0) + add_count
        SURFACE_FAUNA[biome_name] = list(merged.items())
    for surface_biome in biomes:
        if getattr(surface_biome, 'aurora_style', False):
            continue
        surface_biome.animals = [] if is_land_biome(surface_biome) else list(SURFACE_FAUNA.get(surface_biome.name, ()))
    F = flora_types
    surface_flora_profiles = {'Мелководье': [(F['acid_mushroom'], 4.0), (F['blue_palm'], 1.0), (F['writhing_weed'], 2.0)], 'Глубоководье': [(F['ghost_weed'], 1.0), (F['eye_stalk'], 0.7)], 'Центральное море': [(F['ghost_weed'], 0.45), (F['sea_crown'], 0.1)], 'Побережье': [], 'Острова': [], 'Коралловый риф': [(F['spotted_reeds'], 2.2), (F['violet_beau'], 1.8), (F['redwort'], 2.2), (F['blue_palm'], 1.2)], 'Леса': [], 'Кельповый лес': [(F['creepvine'], 6.8), (F['membraintree'], 0.6)], 'Био-люминесцентная зона': [(F['bloodvine'], 2.5), (F['deep_shroom'], 2.8), (F['ghost_weed'], 2.5), (F['anchor_pod'], 1.2)], 'Горы': [], 'Озёра (внешние)': [(F['acid_mushroom'], 2.3), (F['writhing_weed'], 1.3)], 'Реки': [(F['writhing_weed'], 1.4)], 'Притоки': [(F['blue_palm'], 0.6)], 'Водопады': [(F['blue_palm'], 0.7)]}
    cave_flora_profiles = {'Мелководье': [(F['sulfur_plant'], 2.0), (F['acid_mushroom'], 2.0)], 'Глубоководье': [(F['drooping_stinger'], 1.5), (F['ghost_weed'], 1.0)], 'Центральное море': [(F['sea_crown'], 0.5), (F['ghost_weed'], 1.8)], 'Побережье': [(F['sulfur_plant'], 1.0), (F['cave_bush'], 0.9)], 'Острова': [(F['cave_bush'], 2.0), (F['eye_stalk'], 1.0)], 'Коралловый риф': [(F['jellyshroom'], 2.0), (F['redwort'], 1.4)], 'Леса': [(F['cave_bush'], 2.0), (F['drooping_stinger'], 1.0)], 'Кельповый лес': [(F['eye_stalk'], 2.0), (F['cave_bush'], 1.2)], 'Био-люминесцентная зона': [(F['deep_shroom'], 3.0), (F['ghost_weed'], 3.0)], 'Горы': [(F['cave_bush'], 2.0), (F['jellyshroom'], 1.0)], 'Озёра (внешние)': [(F['acid_mushroom'], 1.0)], 'Реки': [(F['eye_stalk'], 0.5)]}
    for surface_biome in biomes:
        if getattr(surface_biome, 'aurora_style', False):
            continue
        surface_biome.flora = [] if is_land_biome(surface_biome) else list(surface_flora_profiles.get(surface_biome.name, ()))

    def find_surface_biome(name, wreck_id):
        candidates = [b for b in biomes if b.name == name and (not getattr(b, 'aurora_style', False))]
        if not candidates:
            return None
        stable_index = sum(((i + 1) * ord(ch) for i, ch in enumerate(str(wreck_id))))
        return candidates[stable_index % len(candidates)]

    def wreck_point_in_biome(parent, original_x, original_z, seed):
        cx, cy = parent.center()
        x1, y1, x2, y2 = parent.rect
        rx = max(8.0, (x2 - x1) * 0.4)
        ry = max(8.0, (y2 - y1) * 0.4)
        angle = math.atan2(-original_z, original_x)
        rnd = random.Random(seed)
        distance = rnd.uniform(0.28, 0.78)
        px = cx + math.cos(angle) * rx * distance
        py = cy + math.sin(angle) * ry * distance
        for _ in range(40):
            if parent.contains(px, py):
                return (int(px), int(py))
            px = (px + cx) / 2.0
            py = (py + cy) / 2.0
        return (int(cx), int(cy))

    def add_wreck_piece(name, rect, passable, priority):
        wreck_biome = Biome(name, AURORA_FLOOR if passable else AURORA_HULL, *rect, [(titan_type, 2), (quartz_type, 1)] if passable else [], [], passable=passable, cave_enabled=False, priority=priority)
        wreck_biome.aurora_style = True
        wreck_biome.location_kind = 'aurora_wreck'
        biomes.append(wreck_biome)
        return wreck_biome

    def add_wreck_structure(wreck_id, parent_name, original_x, original_z, large=True):
        parent = find_surface_biome(parent_name, wreck_id)
        if parent is None:
            return
        seed = 910000 + sum((ord(ch) for ch in str(wreck_id))) * 37 + abs(int(original_x)) * 3 + abs(int(original_z)) * 5
        cx, cy = wreck_point_in_biome(parent, original_x, original_z, seed)
        if large:
            width = 14
            height = 10
        else:
            width = 7
            height = 5
        x1 = max(1, cx - width // 2)
        y1 = max(1, cy - height // 2)
        x2 = min(MAP_SIZE - 2, x1 + width - 1)
        y2 = min(MAP_SIZE - 2, y1 + height - 1)
        label = f'Обломок Авроры №{wreck_id}'
        floor = add_wreck_piece(label, (x1 + 1, y1 + 1, x2 - 1, y2 - 1), True, 82 if large else 78)
        floor.original_subnautica_xz = (original_x, original_z)
        floor.wreck_id = str(wreck_id)
        floor.parent_surface_biome = parent_name
        floor.is_large_wreck = bool(large)
        door_a = (x1 + x2) // 2
        if door_a - 1 >= x1:
            add_wreck_piece(label + ' — северная стенка L', (x1, y1, door_a - 1, y1), False, 90)
        if door_a + 1 <= x2:
            add_wreck_piece(label + ' — северная стенка R', (door_a + 1, y1, x2, y1), False, 90)
        if large:
            door_b = max(x1 + 1, min(x2 - 1, door_a + 2))
            if door_b - 1 >= x1:
                add_wreck_piece(label + ' — южная стенка L', (x1, y2, door_b - 1, y2), False, 90)
            if door_b + 1 <= x2:
                add_wreck_piece(label + ' — южная стенка R', (door_b + 1, y2, x2, y2), False, 90)
        else:
            add_wreck_piece(label + ' — южная стенка', (x1, y2, x2, y2), False, 90)
        add_wreck_piece(label + ' — левая стенка', (x1, y1 + 1, x1, y2 - 1), False, 90)
        add_wreck_piece(label + ' — правая стенка', (x2, y1 + 1, x2, y2 - 1), False, 90)
        if large:
            rnd = random.Random(seed + 17)
            for debris_index in range(3):
                dx = rnd.choice((-1, 1)) * rnd.randint(width // 2 + 2, width + 5)
                dy = rnd.randint(-height, height)
                debris_x = max(1, min(MAP_SIZE - 3, cx + dx))
                debris_y = max(1, min(MAP_SIZE - 3, cy + dy))
                if parent.contains(debris_x, debris_y):
                    add_wreck_piece(label + f' — фрагмент {debris_index + 1}', (debris_x, debris_y, debris_x + rnd.randint(1, 3), debris_y + rnd.randint(1, 2)), False, 84)
    LARGE_WRECKS = [('1', 'Побережье', 320, 431), ('2', 'Побережье', -630, -2), ('3', 'Глубоководье', 910, 600), ('4', 'Центральное море', -128, 884), ('5', 'Горы', 701, 1224), ('6', 'Побережье', -1448, 732), ('7', 'Глубоководье', -1148, -706), ('8', 'Био-люминесцентная зона', -1208, -390), ('9', 'Леса', -672, 750), ('10', 'Коралловый риф', -290, -773), ('11', 'Коралловый риф', -865, -1390), ('12', 'Побережье', -393, 630), ('13', 'Побережье', -15, -624), ('14', 'Побережье', -418, -261), ('15', 'Мелководье', -33, -398), ('16', 'Мелководье', 343, -190), ('17', 'Кельповый лес', 64, 376), ('18', 'Кельповый лес', -317, 223), ('19', 'Горы', 1074, 1335), ('20', 'Коралловый риф', -781, -730), ('43', 'Побережье', -1214, 11), ('44', 'Побережье', -1625, 100), ('45', 'Глубоководье', 1319, 567)]
    for wreck_id, parent_name, original_x, original_z in LARGE_WRECKS:
        add_wreck_structure(wreck_id, parent_name, original_x, original_z, large=True)
    SMALL_WRECKS = [('S1', 'Мелководье', 20, 300), ('S2', 'Мелководье', 90, 120), ('S3', 'Мелководье', 65, -85), ('S4', 'Мелководье', -160, -225), ('S5', 'Мелководье', 175, -253), ('S6', 'Мелководье', 310, -119), ('G1', 'Побережье', 165, -520), ('G2', 'Побережье', 322, 222), ('G3', 'Побережье', 270, 70), ('G4', 'Побережье', -370, 475), ('G5', 'Побережье', -284, 606), ('G6', 'Побережье', -520, -215)]
    for wreck_id, parent_name, original_x, original_z in SMALL_WRECKS:
        add_wreck_structure(wreck_id, parent_name, original_x, original_z, large=False)
    add_wreck_structure('CZ', 'Острова', 527, -43, large=False)
    registry = {}

    def register_start_item(variable_name, value):
        if not isinstance(value, (ObjectType, AnimalType)):
            return
        registry.setdefault(value.name, value)
        if variable_name:
            registry.setdefault(variable_name, value)
    for variable_name, value in list(globals().items()):
        register_start_item(variable_name, value)
    for variable_name, value in list(locals().items()):
        register_start_item(variable_name, value)
    for recipe in recipes:
        register_start_item(None, recipe.result)
        for ingredient in recipe.ingredients:
            register_start_item(None, ingredient)
    for biome in biomes:
        for entry in biome.objects:
            if isinstance(entry, (tuple, list)) and entry:
                register_start_item(None, entry[0])
        for entry in biome.animals:
            if isinstance(entry, (tuple, list)) and entry:
                register_start_item(None, entry[0])
    START_ITEM_REGISTRY = registry

def generate_mad_moss_sprite(size=180, seed=413):
    """Объёмная розетка мха из десятков листочков, побегов, спор и мицелия.

    Спрайт высокого разрешения масштабируется без дорогой регенерации
    в каждом кадре. Контуры колонии разрастаются во все стороны.
    """
    size = max(40, int(size))
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    draw = pygame.draw
    rng = random.Random(seed)
    center = size / 2
    radius = size * 0.34

    def point(x, y):
        return (round(x), round(y))
    for i in range(45):
        a = math.tau * i / 45 + rng.uniform(-0.16, 0.16)
        r1 = radius * rng.uniform(0.21, 0.43)
        r2 = radius * rng.uniform(0.75, 1.19)
        bend = rng.uniform(-0.28, 0.28)
        p0 = point(center + math.cos(a) * r1, center + math.sin(a) * r1)
        p1 = point(center + math.cos(a + bend) * r2 * 0.7, center + math.sin(a + bend) * r2 * 0.7)
        p2 = point(center + math.cos(a) * r2, center + math.sin(a) * r2)
        draw.lines(surf, (16, 65, 37, 215), False, [p0, p1, p2], max(1, size // 130))
        draw.circle(surf, (127, 218, 102, 235), p2, max(1, round(size * rng.uniform(0.009, 0.019))))
    for layer in range(3):
        count = (25, 35, 47)[layer]
        for i in range(count):
            a = i * math.tau / count + rng.uniform(-0.14, 0.14)
            r = radius * rng.uniform(0.2, 0.89) * (1.0 - layer * 0.07)
            x = center + math.cos(a) * r
            y = center + math.sin(a) * r
            rr = size * rng.uniform(0.045, 0.093) * (1.0 - layer * 0.13)
            shadow = ((19, 75, 39, 244), (27, 113, 52, 250), (48, 146, 58, 247))[layer]
            top = ((42, 135, 49, 242), (74, 181, 55, 250), (151, 226, 83, 254))[layer]
            draw.circle(surf, shadow, point(x + rr * 0.16, y + rr * 0.16), round(rr * 1.1))
            draw.circle(surf, top, point(x - rr * 0.1, y - rr * 0.12), max(1, round(rr * 0.78)))
            if i % 2 == 0:
                draw.ellipse(surf, (190, 247, 123, 240), (round(x - rr * 0.4), round(y - rr * 0.58), max(1, round(rr * 0.45)), max(1, round(rr * 0.55))))
    draw.circle(surf, (20, 93, 47, 252), point(center, center), round(radius * 0.37))
    draw.circle(surf, (81, 185, 61, 250), point(center - 1, center - 1), round(radius * 0.27))
    for i in range(33):
        a = math.tau * i / 33 + rng.uniform(-0.1, 0.1)
        r = radius * rng.uniform(0.3, 0.85)
        p0 = point(center + math.cos(a) * radius * 0.11, center + math.sin(a) * radius * 0.11)
        p1 = point(center + math.cos(a) * r, center + math.sin(a) * r)
        draw.aaline(surf, (198, 244, 118, 220), p0, p1)
        if i % 3 == 0:
            draw.circle(surf, (236, 251, 167, 245), p1, max(1, size // 125))
    return surf
_LAND_TREE_SCANNER_TYPES = {}

def _land_tree_scanner_type(species_index):
    species_index = int(species_index)
    result = _LAND_TREE_SCANNER_TYPES.get(species_index)
    if result is None:
        result = ObjectType(TREE_SPECIES[species_index], (71, 155, 71), 66, 'circle')
        result.image = generate_tree_sprite(species_index).convert_alpha()
        _LAND_TREE_SCANNER_TYPES[species_index] = result
    return result

class MadMossColony:
    """Независимая лесная колония. Маленькая конечная колония не растёт.

    Каждый удар по крупной колонии заменяет её ДВУМЯ меньшими.
    Подбирать можно только конечные колонии диаметром ровно 0.2 клетки.
    """
    __slots__ = ('x', 'y', 'diameter', 'heading', 'turn_timer', 'home_biome', 'terminal', 'age', 'variant')

    def __init__(self, x, y, home_biome, diameter=1.15, terminal=False):
        self.x, self.y = (float(x), float(y))
        self.home_biome = home_biome
        self.diameter = max(MAD_MOSS_MIN_DIAMETER, float(diameter))
        self.terminal = bool(terminal or self.diameter <= MAD_MOSS_MIN_DIAMETER + 1e-06)
        if self.terminal:
            self.diameter = MAD_MOSS_MIN_DIAMETER
        self.heading = random.uniform(-math.pi, math.pi)
        self.turn_timer = random.randint(15, 70)
        self.age = random.randrange(200)
        self.variant = random.randint(0, 5)

    def update(self, world):
        self.age += 1
        self.turn_timer -= 1
        if self.turn_timer <= 0:
            self.heading = random.uniform(-math.pi, math.pi)
            self.turn_timer = random.randint(18, 75)
        speed = MAD_MOSS_SPEED
        nx = self.x + math.cos(self.heading) * speed
        ny = self.y + math.sin(self.heading) * speed
        if 0 <= nx < MAP_SIZE and 0 <= ny < MAP_SIZE and (world.get_biome_at(nx, ny) is self.home_biome) and self.home_biome.passable and (not world.get_object_at(nx, ny)) and (not getattr(world, 'creature_blocked', lambda x, y: False)(int(nx), int(ny))):
            self.x, self.y = (nx, ny)
        else:
            self.heading += math.pi + random.uniform(-0.55, 0.55)
            self.turn_timer = random.randint(10, 32)
        if not self.terminal:
            self.diameter = min(MAD_MOSS_MAX_DIAMETER, self.diameter + MAD_MOSS_GROWTH_PER_FRAME)
_MAD_MOSS_SPRITE_CACHE = {}

def get_mad_moss_sprite(pixel_diameter, variant=0):
    """Кэш размера/цвета; в игре рисуется без пересоздания 100+ деталей."""
    diameter = max(4, int(round(pixel_diameter)))
    key = (diameter, int(variant) % 6)
    sprite = _MAD_MOSS_SPRITE_CACHE.get(key)
    if sprite is None:
        source = generate_mad_moss_sprite(180, 413 + key[1] * 89)
        sprite = pygame.transform.smoothscale(source, (diameter, diameter)).convert_alpha()
        if len(_MAD_MOSS_SPRITE_CACHE) > 280:
            _MAD_MOSS_SPRITE_CACHE.clear()
        _MAD_MOSS_SPRITE_CACHE[key] = sprite
    return sprite

def generate_walker_sprite(size=30, stride=0):
    """Вид сверху, пешеход в снаряжении: голова, ранец, руки и две ноги.

    В отличие от пловца, здесь нет баллонов по бокам и ласт, зато видны
    ботинки. Значение stride (-1, 0, +1) даёт простую анимацию походки.
    Герой смотрит вправо; Game поворачивает спрайт вслед за курсором.
    """
    w, h = (int(size * 2.0), int(size * 1.8))
    surf = pygame.Surface((w, h), pygame.SRCALPHA)
    cx, cy = (w // 2, h // 2)
    dark = (26, 34, 42)
    suit = (44, 108, 173)
    suit_hi = (81, 163, 210)
    boots = (43, 44, 52)
    pygame.draw.ellipse(surf, (0, 0, 0, 58), (cx - 20, cy - 14, 42, 29))
    for side in (-1, 1):
        step = int(stride * side * 5)
        yy = cy + side * 10
        pygame.draw.line(surf, dark, (cx - 5, yy), (cx - 16 + step, yy), 6)
        pygame.draw.ellipse(surf, boots, (cx - 23 + step, yy - 4, 10, 8))
        pygame.draw.line(surf, (137, 147, 154), (cx - 21 + step, yy - 3), (cx - 18 + step, yy - 3), 1)
    for side in (-1, 1):
        yy = cy + side * 18
        hand_shift = -stride * side * 3
        pygame.draw.line(surf, (24, 64, 106), (cx + 1, cy + side * 9), (cx + 10 + hand_shift, yy), 7)
        pygame.draw.circle(surf, (191, 199, 187), (cx + 10 + int(hand_shift), yy), 4)
    pygame.draw.ellipse(surf, dark, (cx - 20, cy - 12, 30, 24))
    pygame.draw.ellipse(surf, suit, (cx - 14, cy - 11, 31, 22))
    pygame.draw.rect(surf, (80, 91, 91), (cx - 20, cy - 10, 13, 20), border_radius=3)
    pygame.draw.line(surf, (248, 203, 83), (cx - 6, cy - 7), (cx + 9, cy - 7), 2)
    pygame.draw.line(surf, suit_hi, (cx - 4, cy + 6), (cx + 9, cy + 6), 2)
    pygame.draw.circle(surf, (20, 28, 40), (cx + 17, cy), 12)
    pygame.draw.circle(surf, (221, 193, 92), (cx + 17, cy), 10)
    pygame.draw.circle(surf, (36, 111, 144), (cx + 19, cy), 8)
    pygame.draw.ellipse(surf, (123, 224, 237), (cx + 15, cy - 6, 8, 4))
    pygame.draw.arc(surf, (244, 237, 188), (cx + 7, cy - 10, 20, 20), -1.3, 1.3, 2)
    return surf.convert_alpha()

def generate_tree_sprite(species, variant=0):
    """Крупное дерево сверху, без внешних текстур. Все шесть видов различимы."""
    seed = 41921 + species * 1301 + variant * 719
    rng = random.Random(seed)
    size = 66
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    c = size // 2
    pygame.draw.ellipse(surf, (0, 0, 0, 47), (c - 23, c - 18, 50, 49))
    pygame.draw.circle(surf, (76, 55, 40), (c + 2, c + 3), 7)
    pygame.draw.circle(surf, (131, 92, 53), (c + 1, c + 2), 4)
    if species in (0, 1):
        base = (25, 85, 58) if species == 0 else (23, 74, 64)
        light = (69, 143, 74) if species == 0 else (59, 126, 110)
        for ring, arms in ((22, 11), (16, 9), (9, 7)):
            for i in range(arms):
                a = math.tau * i / arms + rng.uniform(-0.1, 0.1)
                end = (c + math.cos(a) * ring, c + math.sin(a) * ring)
                side = 0.28 if species == 0 else 0.2
                pygame.draw.polygon(surf, base, [(c, c), (c + math.cos(a - side) * (ring - 2), c + math.sin(a - side) * (ring - 2)), end, (c + math.cos(a + side) * (ring - 2), c + math.sin(a + side) * (ring - 2))])
        pygame.draw.circle(surf, light, (c - 3, c - 4), 6)
        pygame.draw.circle(surf, (125, 177, 102), (c - 4, c - 6), 3)
    elif species in (2, 3, 4):
        colors = {2: ((28, 91, 48), (67, 144, 63), (110, 178, 84)), 3: ((52, 124, 68), (115, 178, 95), (177, 215, 127)), 4: ((102, 41, 70), (182, 65, 83), (223, 122, 98))}[species]
        for i in range(13):
            a = i * math.tau / 13 + rng.uniform(-0.22, 0.22)
            r = rng.uniform(9, 19)
            px = c + int(math.cos(a) * r)
            py = c + int(math.sin(a) * r)
            pygame.draw.circle(surf, colors[i % 2], (px, py), rng.randint(10, 15))
        pygame.draw.circle(surf, colors[1], (c, c), 16)
        for i in range(7):
            a = rng.random() * math.tau
            r = rng.uniform(0, 13)
            pygame.draw.circle(surf, colors[2], (round(c + math.cos(a) * r), round(c + math.sin(a) * r)), rng.randint(3, 7))
        if species == 3:
            pygame.draw.line(surf, (230, 229, 213), (c - 2, c - 2), (c + 5, c + 5), 2)
    else:
        for i in range(9):
            a = i * math.tau / 9 + rng.uniform(-0.09, 0.09)
            r = 25 + rng.randint(-4, 3)
            ex = c + math.cos(a) * r
            ey = c + math.sin(a) * r
            pygame.draw.line(surf, (39, 97, 52), (c, c), (ex, ey), 6)
            pygame.draw.line(surf, (109, 180, 77), (c - 2, c - 2), (ex - 3, ey - 3), 2)
            for t in (0.4, 0.65, 0.8):
                bx, by = (c + math.cos(a) * r * t, c + math.sin(a) * r * t)
                for sign in (-1, 1):
                    pygame.draw.line(surf, (74, 146, 63), (bx, by), (bx + math.cos(a + sign * 0.9) * 8, by + math.sin(a + sign * 0.9) * 8), 2)
        pygame.draw.circle(surf, (185, 142, 67), (c, c), 5)
    return surf.convert_alpha()

class GolemScanEntity:
    """Не является предметом: картинка только в большом экране сканера."""

    def __init__(self, kind, bacterium=False):
        self.kind = kind
        self.bacterium = bacterium
        self.name = 'Големогенные бактерии — ' + {'stone': 'каменные', 'water': 'водяные', 'wood': 'деревянные', 'dung': 'говняные'}[kind] if bacterium else GOLEM_DATA[kind]['name']
        self.size = 1 if bacterium else int(30 * GOLEM_DATA[kind]['diameter'])
        self.color = GOLEM_DATA[kind]['color']
        self.image = None
GOLEM_SCAN_TYPES = {k: GolemScanEntity(k) for k in GOLEM_DATA}
GOLEM_BACTERIA_SCAN_TYPES = {k: GolemScanEntity(k, True) for k in GOLEM_DATA}
_GOLEM_SPRITE_CACHE = {}

def generate_golem_sprite(kind, pixel_size=200, shattered=False):
    """Высокодетализированный голем в Pygame: вид сверху, голова направо."""
    n = 240
    surf = pygame.Surface((n, n), pygame.SRCALPHA)
    d = pygame.draw
    seed = {'stone': 13, 'water': 29, 'wood': 41, 'dung': 73}[kind]
    rnd = random.Random(seed)
    base = GOLEM_DATA[kind]['color']
    edge = tuple((max(0, v - 50) for v in base))
    light = tuple((min(255, v + 48) for v in base))
    gl = {'stone': (116, 240, 218), 'water': (165, 255, 255), 'wood': (139, 251, 111), 'dung': (250, 207, 72)}[kind]
    for side in (-1, 1):
        yy = 120 + side * 48
        d.line(surf, edge, (84, yy), (45, yy + side * 45), 25)
        d.line(surf, base, (84, yy), (44, yy + side * 45), 18)
        d.circle(surf, light, (44, yy + side * 45), 14)
        d.line(surf, edge, (145, yy), (183, yy + side * 32), 26)
        d.line(surf, base, (145, yy), (181, yy + side * 32), 18)
        d.circle(surf, light, (185, yy + side * 33), 13)
    if kind == 'stone':
        for x, y, w, h in [(66, 80, 78, 80), (105, 65, 67, 75), (88, 116, 79, 60), (137, 94, 46, 62), (48, 103, 49, 46)]:
            pts = [(x, y + 9), (x + 12, y), (x + w - 11, y + 2), (x + w, y + h // 3), (x + w - 8, y + h - 3), (x + w // 2, y + h), (x + 2, y + h - 12)]
            d.polygon(surf, edge, pts)
            d.polygon(surf, base, [(xx - 4 if xx > x + w // 2 else xx + 3, yy - 3 if yy > y + h // 2 else yy + 2) for xx, yy in pts])
            d.line(surf, light, (x + 10, y + 9), (x + w - 10, y + 8), 3)
        for i in range(22):
            x = rnd.randint(60, 176)
            y = rnd.randint(80, 160)
            d.line(surf, edge, (x, y), (x + rnd.randint(-12, 12), y + 9), 2)
    elif kind == 'water':
        d.ellipse(surf, (15, 79, 123, 180), (49, 62, 148, 117))
        d.ellipse(surf, (39, 160, 221, 224), (58, 69, 132, 104))
        d.ellipse(surf, (105, 214, 252, 125), (74, 73, 96, 55))
        for i in range(24):
            x = rnd.randrange(65, 185)
            y = rnd.randrange(80, 162)
            r = rnd.randrange(2, 8)
            d.circle(surf, (167, 236, 255, 170), (x, y), r, 1)
        for i in range(5):
            x = 72 + i * 22
            d.arc(surf, (181, 246, 255), (x, 119, 35, 23), 0.3, 2.7, 2)
    elif kind == 'wood':
        d.ellipse(surf, edge, (58, 59, 141, 129))
        d.ellipse(surf, base, (64, 65, 128, 118))
        for off in (-28, -12, 8, 28):
            d.arc(surf, light, (78 + off, 72, 72, 94), 0.85, 5.65, 3)
        for i in range(17):
            x = rnd.randrange(75, 182)
            y = rnd.randrange(76, 167)
            d.line(surf, edge, (x, y), (x - 5, y + rnd.randrange(8, 24)), 2)
        for x, y in [(65, 85), (134, 68), (170, 155), (103, 176)]:
            d.circle(surf, (50, 137, 65), (x, y), 15)
            d.circle(surf, (91, 179, 80), (x - 3, y - 4), 10)
    else:
        for x, y, r in [(101, 132, 51), (132, 108, 55), (159, 137, 36), (88, 108, 32), (123, 81, 29)]:
            d.circle(surf, edge, (x + 3, y + 3), r + 2)
            d.circle(surf, base, (x, y), r)
            d.circle(surf, light, (x - 9, y - 10), max(8, r // 2))
        for i in range(24):
            x = rnd.randint(76, 175)
            y = rnd.randint(77, 159)
            d.circle(surf, (75, 48, 24), (x, y), rnd.randint(2, 5))
        for x, y in [(38, 52), (185, 66), (193, 177)]:
            d.circle(surf, (15, 20, 19), (x, y), 3)
            d.line(surf, (90, 130, 105), (x - 5, y), (x + 5, y), 1)
    nodes = [(87, 120), (106, 91), (132, 112), (151, 133), (117, 152), (171, 94)]
    for a, b in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (2, 4), (2, 5)]:
        d.line(surf, gl, nodes[a], nodes[b], 2)
    for x, y in nodes:
        d.circle(surf, tuple((max(0, v - 90) for v in gl)), (x, y), 5)
        d.circle(surf, gl, (x, y), 3)
    for ey in (102, 145):
        d.circle(surf, edge, (174, ey), 12)
        d.circle(surf, gl, (176, ey), 7)
        d.circle(surf, (253, 254, 241), (179, ey - 3), 2)
    if shattered:
        surf.set_alpha(112)
    cropped = surf.subsurface(surf.get_bounding_rect(min_alpha=1)).copy()
    pixel_size = max(8, int(pixel_size))
    return pygame.transform.smoothscale(cropped, (pixel_size, pixel_size)).convert_alpha()

def generate_golem_bacterium_preview(kind, size=260):
    """Условное изображение под микроскопом: реальный диаметр 1 мкм."""
    surf = pygame.Surface((280, 280), pygame.SRCALPHA)
    d = pygame.draw
    color = GOLEM_DATA[kind]['color']
    d.ellipse(surf, (40, 78, 105, 35), (27, 27, 226, 226), 3)
    for i, (x, y) in enumerate([(132, 116), (74, 178), (192, 181)]):
        w = 64 if i == 0 else 42
        d.ellipse(surf, tuple((max(0, c - 40) for c in color)), (x - w // 2, y - w // 2, w, w))
        d.ellipse(surf, color, (x - w // 2 + 4, y - w // 2 + 4, w - 8, w - 8))
        d.circle(surf, (229, 249, 184), (x - 7, y - 8), max(3, w // 8))
        for a in range(10):
            theta = math.tau * a / 10 + i
            x1 = x + math.cos(theta) * w * 0.47
            y1 = y + math.sin(theta) * w * 0.47
            d.line(surf, (126, 230, 197), (x1, y1), (x1 + math.cos(theta) * 14, y1 + math.sin(theta) * 14), 2)
    return pygame.transform.smoothscale(surf, (size, size)).convert_alpha()

class GolemColony:
    """Невесомые особи -> заражение субстрата -> живой голем -> обломки."""
    __slots__ = ('kind', 'x', 'y', 'anchor_x', 'anchor_y', 'phase', 'mass', 'hp', 'max_hp', 'heading', 'heading_timer', 'next_attack', 'revive_at', 'last_update', 'seed')

    def __init__(self, kind, x, y, phase='spore', seed=0):
        self.kind = kind
        self.x = float(x)
        self.y = float(y)
        self.anchor_x = float(x)
        self.anchor_y = float(y)
        self.phase = phase
        self.mass = GOLEM_FORMATION_MASS if phase == 'active' else 0.0
        self.max_hp = GOLEM_DATA[kind]['hp']
        self.hp = self.max_hp if phase == 'active' else 0
        self.heading = random.Random(seed + 31).uniform(-math.pi, math.pi)
        self.heading_timer = 50
        self.next_attack = 0
        self.revive_at = 0
        self.last_update = 0
        self.seed = seed

    def _substrate_here(self, world, x, y):
        ix, iy = (int(x), int(y))
        b = world.get_biome_at(ix, iy)
        if b is None or not b.passable or getattr(b, 'aurora_style', False):
            return False
        if self.kind == 'water':
            return world.is_water_at(ix, iy)
        if not is_land_biome(b):
            return False
        if self.kind == 'wood':
            return (ix, iy) in world.tree_grid
        if self.kind == 'dung':
            return (ix, iy) in world.golem_dung_piles
        if self.kind == 'stone':
            obj = world.get_object_at(ix, iy)
            return b.name in ('Горы', 'Острова', 'Побережье') or obj is rock_type or obj is limestone_type
        return False

    def update(self, world, player_x, player_y, frame, antibiotics_visible=False):
        elapsed = max(1, min(1200, frame - self.last_update)) if self.last_update else 1
        self.last_update = frame
        rng = random
        if self.phase == 'spore':
            wind = frame * 0.0018
            vx = (math.cos(wind) + 0.45 * math.cos(self.heading)) * GOLEM_WIND_SPEED
            vy = (math.sin(wind * 0.83) + 0.45 * math.sin(self.heading)) * GOLEM_WIND_SPEED
            self.x = min(MAP_SIZE - 2, max(1, self.x + vx * min(elapsed, 30)))
            self.y = min(MAP_SIZE - 2, max(1, self.y + vy * min(elapsed, 30)))
            if self._substrate_here(world, self.x, self.y):
                self.phase = 'breeding'
                self.anchor_x, self.anchor_y = (self.x, self.y)
                self.mass = max(0, self.mass)
        elif self.phase == 'breeding':
            if not self._substrate_here(world, self.anchor_x, self.anchor_y):
                self.phase = 'spore'
                self.mass = 0
            else:
                self.mass += min(elapsed, 1200) * 0.29
                if self.mass >= GOLEM_FORMATION_MASS:
                    self.phase = 'active'
                    self.hp = self.max_hp
                    self.x, self.y = (self.anchor_x, self.anchor_y)
                    self.heading_timer = 0
        elif self.phase == 'rubble':
            if frame >= self.revive_at:
                if self._substrate_here(world, self.anchor_x, self.anchor_y):
                    self.phase = 'breeding'
                    self.mass = 70.0
                else:
                    self.phase = 'spore'
                    self.mass = 0.0
        elif self.phase == 'active':
            self.heading_timer -= 1
            dx = player_x - self.x
            dy = player_y - self.y
            dist = math.hypot(dx, dy)
            if dist < 5.5 and dist > 0.1:
                angle = math.atan2(dy, dx)
                if antibiotics_visible and dist < 3.5:
                    angle += math.pi
                elif self.hp < self.max_hp * 0.35 and dist < 2.5:
                    angle += math.pi * 0.8
            else:
                angle = self.heading
            if self.heading_timer <= 0:
                self.heading = rng.uniform(-math.pi, math.pi)
                self.heading_timer = rng.randrange(60, 180)
            speed = GOLEM_DATA[self.kind]['speed']
            if self.kind == 'water' and dist < 4:
                speed *= 1.35
            nx = self.x + math.cos(angle) * speed
            ny = self.y + math.sin(angle) * speed
            b = world.get_biome_at(nx, ny)
            permitted = b is not None and b.passable and (not getattr(b, 'aurora_style', False)) and (not (world.get_object_at(nx, ny) is not None and world.get_object_at(nx, ny).is_wall)) and (world.is_water_at(nx, ny) if self.kind == 'water' else is_land_biome(b))
            blocker = getattr(world, 'creature_blocked', None)
            if permitted and (blocker is None or not blocker(int(nx), int(ny))):
                self.x, self.y = (nx, ny)
            else:
                self.heading += math.pi * 0.69
                self.heading_timer = 12

    def hit(self, damage, frame):
        if self.phase != 'active':
            return False
        self.hp = max(0, self.hp - max(0, damage))
        if self.hp <= 0:
            self.phase = 'rubble'
            self.revive_at = frame + random.randint(GOLEM_REASSEMBLE_MIN, GOLEM_REASSEMBLE_MAX)
            self.anchor_x, self.anchor_y = (self.x, self.y)
        return True

class World:

    def __init__(self):
        self.biomes = biomes
        self.object_grid = {}
        self.flora_grid = {}
        self.tree_grid = {}
        self.moss_colonies = []
        self.golem_colonies = []
        self.golem_dung_piles = set()
        self.animals = []
        self.biome_chunks = {}
        self.biome_cache = {}
        self.animal_chunks = {}
        self._build_biome_index()
        self._generate_objects()
        self._generate_animals()
        self._generate_flora()
        self._generate_trees()
        self._generate_mad_moss()
        self._generate_golem_colonies()
        self.rebuild_animal_index()

    def _build_biome_index(self):
        chunk = WORLD_CHUNK_SIZE
        max_chunk = (MAP_SIZE - 1) // chunk
        declaration_order = {id(biome): index for index, biome in enumerate(self.biomes)}
        for biome in self.biomes:
            x1, y1, x2, y2 = biome.rect
            cx1 = max(0, x1 // chunk)
            cy1 = max(0, y1 // chunk)
            cx2 = min(max_chunk, x2 // chunk)
            cy2 = min(max_chunk, y2 // chunk)
            for cy in range(cy1, cy2 + 1):
                for cx in range(cx1, cx2 + 1):
                    key = (cx, cy)
                    self.biome_chunks.setdefault(key, []).append(biome)
        for candidates in self.biome_chunks.values():
            candidates.sort(key=lambda biome: (biome.priority, declaration_order[id(biome)]), reverse=True)

    def get_biome_at(self, x, y):
        x = int(x)
        y = int(y)
        if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
            return None
        cache_key = (x, y)
        if cache_key in self.biome_cache:
            return self.biome_cache[cache_key]
        chunk_key = (x // WORLD_CHUNK_SIZE, y // WORLD_CHUNK_SIZE)
        result = None
        for biome in self.biome_chunks.get(chunk_key, ()):
            if biome.contains(x, y):
                result = biome
                break
        if len(self.biome_cache) >= MAX_BIOME_CACHE:
            self.biome_cache.clear()
        self.biome_cache[cache_key] = result
        return result

    def is_water_at(self, x, y):
        return not is_land_biome(self.get_biome_at(x, y)) and self.get_biome_at(x, y) is not None

    def _generate_trees(self):
        """Леса густые, но не перекрывают реки и входы в пещеры.

        Деревья не занимают object_grid: доступны постройки, минералы,
        сканер и предметы, а при рисовании игрок всегда поверх кроны.
        """
        self.tree_grid.clear()
        for index, biome in enumerate(self.biomes):
            if biome.name != 'Леса':
                continue
            count = min(3000, int(biome.area() / 400.0 * FOREST_TREES_PER_400_CELLS))
            rnd = random.Random(880083 + 7937 * index)
            for _ in range(count):
                x, y = biome.random_point(rnd)
                x, y = (int(x), int(y))
                key = (x, y)
                if key in self.tree_grid or key in self.object_grid:
                    continue
                if self.get_biome_at(x, y) is not biome:
                    continue
                species = rnd.randrange(len(TREE_SPECIES))
                variant = rnd.randrange(6)
                self.tree_grid[key] = (species, variant)

    def _generate_mad_moss(self):
        """Мох растёт только в фактически видимых лесных биомах."""
        self.moss_colonies.clear()
        for index, biome in enumerate(self.biomes):
            if biome.name != 'Леса' or not biome.passable:
                continue
            quantity = min(38, max(1, int(biome.area() / 400 * MAD_MOSS_PER_400_FOREST)))
            rnd = random.Random(9613 + index * 661)
            for _ in range(quantity):
                if len(self.moss_colonies) >= MAD_MOSS_MAX_COLONIES:
                    return
                for attempt in range(80):
                    x, y = biome.random_point(rnd)
                    if self.get_biome_at(x, y) is biome and (int(x), int(y)) not in self.object_grid:
                        self.moss_colonies.append(MadMossColony(x, y, biome, diameter=rnd.uniform(0.75, 1.25)))
                        break

    def _generate_golem_colonies(self):
        """Ветер приносит микробы к реальным субстратам по всей карте.

        Часть популяций уже построила големов, остальные будут размножаться.
        Никакой рыбной фауны на суше при этом не появляется.
        """
        self.golem_colonies.clear()
        self.golem_dung_piles.clear()
        if not GOLEM_ENABLED:
            return
        forest_trees = {}
        for (x, y), tree in self.tree_grid.items():
            biome = self.get_biome_at(x, y)
            if biome is not None:
                forest_trees.setdefault(biome, []).append((x, y))
        for i, biome in enumerate(self.biomes):
            if len(self.golem_colonies) >= GOLEM_MAX_COLONIES:
                break
            if not biome.passable or getattr(biome, 'aurora_style', False):
                continue
            k = biome.name
            if k == 'Леса':
                kinds = ('wood', 'dung', 'stone')
            elif k in ('Горы', 'Острова', 'Побережье'):
                kinds = ('stone', 'dung')
            elif not is_land_biome(biome):
                kinds = ('water',)
            else:
                continue
            rnd = random.Random(800517 + i * 1777)
            count = max(1, min(8, int(biome.area() / 2300) + 1))
            if k == 'Центральное море':
                count = min(count, 5)
            for j in range(count):
                if len(self.golem_colonies) >= GOLEM_MAX_COLONIES:
                    break
                kind = kinds[j % len(kinds)]
                spot = None
                for attempt in range(42):
                    if kind == 'wood' and forest_trees.get(biome):
                        x, y = rnd.choice(forest_trees[biome])
                    else:
                        x, y = biome.random_point(rnd)
                        x, y = (int(x), int(y))
                    if self.get_biome_at(x, y) is not biome:
                        continue
                    if self.get_object_at(x, y) is not None and self.get_object_at(x, y).is_wall:
                        continue
                    if kind == 'dung':
                        self.golem_dung_piles.add((x, y))
                    candidate = GolemColony(kind, x, y, 'active' if j % 3 == 0 else 'spore', seed=i * 997 + j)
                    if candidate._substrate_here(self, x, y):
                        spot = candidate
                        break
                if spot is not None:
                    if spot.phase == 'spore':
                        spot.x += rnd.uniform(-0.15, 0.15)
                        spot.y += rnd.uniform(-0.15, 0.15)
                    self.golem_colonies.append(spot)

    def get_object_at(self, x, y):
        return self.object_grid.get((int(x), int(y)))

    def remove_object_at(self, x, y):
        return self.object_grid.pop((int(x), int(y)), None)

    def _generate_objects(self):
        object_grid = self.object_grid
        for biome in self.biomes:
            x1, y1, x2, y2 = biome.rect
            area = biome.area()
            for object_type, density in biome.objects:
                # Увеличенный запас строительных материалов в мире.
                # Редкие сюжетные предметы, оружие и декор не размножаем.
                building_materials = {
                    'Камень', 'Титан', 'Медная руда', 'Железная руда',
                    'Золотая руда', 'Кварц', 'Серебряная руда', 'Свинец',
                    'Литий', 'Известняк', 'Песок', 'Глина', 'Уголь',
                    'Алмаз', 'Соль', 'Металлолом', 'Кислотный гриб',
                    'Образец водоросли', 'Гроздь семян водоросли',
                    'Образец пластинчатого коралла', 'Образец коралловой трубы',
                    'Пещерная сера', 'Магнетит', 'Рубин', 'Никелевая руда',
                    'Кианит', 'Уранинит'
                }
                multiplier = 2.1 if object_type.name in building_materials else 1.0
                count = int(area / 400 * density * multiplier)
                for _ in range(count):
                    x = random.randint(x1, x2)
                    y = random.randint(y1, y2)
                    key = (x, y)
                    effective_biome = self.get_biome_at(x, y)
                    if effective_biome is biome and biome.passable and (key not in object_grid):
                        object_grid[key] = object_type
        self._spawn_weapons_in_large_wrecks()

    def _spawn_weapons_in_large_wrecks(self):
        weapon_types = [laser_pistol, underwater_rifle, plasma_cannon, lang_cannon, schweer_cannon, diamond_cutter, flamethrower, golden_pistol]
        large_wrecks = [biome for biome in self.biomes if getattr(biome, 'is_large_wreck', False)]
        rng = random
        for group_start in range(0, len(large_wrecks), 3):
            group = large_wrecks[group_start:group_start + 3]
            if len(group) < 3:
                break
            wreck = rng.choice(group)
            weapon = rng.choice(weapon_types)
            x1, y1, x2, y2 = wreck.rect
            candidates = []
            for y in range(y1, y2 + 1):
                for x in range(x1, x2 + 1):
                    key = (x, y)
                    if self.get_biome_at(x, y) is wreck and key not in self.object_grid:
                        candidates.append(key)
            if candidates:
                self.object_grid[rng.choice(candidates)] = weapon

    def _generate_animals(self):
        append = self.animals.append
        for biome in self.biomes:
            if is_land_biome(biome):
                continue
            for animal_type, count in biome.animals:
                if animal_type.schooling:
                    count = int(count * FISH_COUNT_MULTIPLIER)
                for _ in range(count):
                    spawn = None
                    for _attempt in range(100):
                        x, y = biome.random_point()
                        if self.get_biome_at(int(x), int(y)) is biome:
                            spawn = (x, y)
                            break
                    if spawn is None:
                        continue
                    append(Animal(spawn[0], spawn[1], animal_type, biome))

    def _generate_flora(self):
        """Детерминированно размещает декоративную флору поверхности."""
        self.flora_grid.clear()
        for biome_index, biome in enumerate(self.biomes):
            if is_land_biome(biome):
                continue
            if not getattr(biome, 'flora', None):
                continue
            if not biome.passable:
                continue
            area = max(1.0, biome.area())
            for flora_index, (flora_type, density) in enumerate(biome.flora):
                count = int(area / 400.0 * float(density))
                if density > 0 and count <= 0:
                    count = 1
                count = min(count, 2200)
                seed = 310000 + biome_index * 10007 + flora_index * 379 + sum(((i + 1) * ord(ch) for i, ch in enumerate(biome.name)))
                rnd = random.Random(seed)
                placed = 0
                attempts = 0
                max_attempts = max(40, count * 12)
                while placed < count and attempts < max_attempts:
                    attempts += 1
                    rx, ry = biome.random_point(rnd)
                    x = int(rx)
                    y = int(ry)
                    key = (x, y)
                    if self.get_biome_at(x, y) is not biome:
                        continue
                    if key in self.object_grid or key in self.flora_grid:
                        continue
                    self.flora_grid[key] = flora_type
                    placed += 1

    @staticmethod
    def _animal_chunk_key(x, y):
        return (int(x) // ANIMAL_CHUNK_SIZE, int(y) // ANIMAL_CHUNK_SIZE)

    def rebuild_animal_index(self):
        self.animal_chunks.clear()
        for order, animal in enumerate(self.animals):
            animal.world_order = order
            key = self._animal_chunk_key(animal.x, animal.y)
            self.animal_chunks.setdefault(key, set()).add(animal)

    def get_animals_near(self, x, y, radius):
        """Возвращает животных только из соседних пространственных чанков."""
        chunk = ANIMAL_CHUNK_SIZE
        min_cx = int(math.floor(x - radius)) // chunk
        max_cx = int(math.floor(x + radius)) // chunk
        min_cy = int(math.floor(y - radius)) // chunk
        max_cy = int(math.floor(y + radius)) // chunk
        result = []
        append = result.append
        for cy in range(min_cy, max_cy + 1):
            for cx in range(min_cx, max_cx + 1):
                for animal in self.animal_chunks.get((cx, cy), ()):
                    if abs(animal.x - x) <= radius and abs(animal.y - y) <= radius:
                        append(animal)
        result.sort(key=lambda animal: animal.world_order)
        return result

    def update_animal_index(self, animal, old_x, old_y):
        old_key = self._animal_chunk_key(old_x, old_y)
        new_key = self._animal_chunk_key(animal.x, animal.y)
        if old_key == new_key:
            return
        old_bucket = self.animal_chunks.get(old_key)
        if old_bucket is not None:
            old_bucket.discard(animal)
            if not old_bucket:
                self.animal_chunks.pop(old_key, None)
        self.animal_chunks.setdefault(new_key, set()).add(animal)

    def remove_animal(self, animal):
        key = self._animal_chunk_key(animal.x, animal.y)
        bucket = self.animal_chunks.get(key)
        if bucket is not None:
            bucket.discard(animal)
            if not bucket:
                self.animal_chunks.pop(key, None)
        try:
            self.animals.remove(animal)
        except ValueError:
            pass

class Animal:

    def __init__(self, x, y, animal_type, home_biome):
        self.x = x
        self.y = y
        self.type = animal_type
        self.home_biome = home_biome
        self.swim_heading = random.uniform(0.0, math.tau)
        self.cruise_heading = self.swim_heading
        self.swim_phase = random.uniform(0.0, math.tau)
        self.speed_phase = random.uniform(0.0, math.tau)
        self.phase_rate = random.uniform(0.075, 0.135)
        self.swim_turn_sign = random.choice((-1, 1))
        self.fleeing = False
        self.stuck_frames = 0
        self.move_timer = random.randint(28, 75)
        self.dx = math.cos(self.swim_heading) * animal_type.speed
        self.dy = math.sin(self.swim_heading) * animal_type.speed
        self.active = False
        self.rotated_image = None
        self.rotated_glow = None
        self.hp = animal_type.hp
        self.max_hp = animal_type.hp
        self.attack_timer = 0
        self.attack_cooldown = 60
        self.is_companion = False
        self.companion_orbit_angle = random.uniform(0.0, math.tau)
        self.companion_orbit_radius = random.uniform(1.8, 2.7)
        self.companion_target = None
        self.companion_attack_timer = 0
        self.companion_attack_cooldown = 28
        self.companion_state = 'свободно плавает'
        self.pet_combat_target = None
        self._update_rotation()

    def _update_rotation(self):
        if self.type.image is None:
            return
        if self.dx == 0 and self.dy == 0:
            angle = 0
        elif self.type.fish_style:
            angle = -math.degrees(math.atan2(self.dy, self.dx))
        else:
            angle = math.degrees(math.atan2(self.dy, self.dx) + math.pi / 2)
        self.visual_angle = angle
        self.rotated_image = self.type.get_rotated(angle)
        self.rotated_glow = self.type.get_rotated_glow(angle)

    @staticmethod
    def _angle_difference(target, current):
        return math.atan2(math.sin(target - current), math.cos(target - current))

    def _can_swim_to(self, x, y, world):
        """Одна проверка для стен, границ, пещер и стартовой капсулы."""
        if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
            return False
        cave_lookup = getattr(world, 'cave_lookup', None)
        if cave_lookup is not None:
            if cave_lookup(x, y) is None:
                return False
        else:
            if not self.home_biome.contains(x, y):
                return False
            if is_land_biome(self.home_biome):
                if world.get_biome_at(x, y) is not self.home_biome:
                    return False
            elif not world.is_water_at(x, y):
                return False
        obj = world.get_object_at(int(x), int(y))
        if obj is not None and obj.is_wall:
            return False
        extra_wall = getattr(world, 'creature_blocked', None)
        return extra_wall is None or not extra_wall(int(x), int(y))

    def _find_open_heading(self, desired, speed, world):
        """При упоре в препятствие выбираем направление со свободным путём.

        Сканирование проводится ТОЛЬКО при упоре, а не для каждой рыбы
        каждый кадр, поэтому большая популяция не тормозит игру.
        """
        sign = self.swim_turn_sign
        offsets = (0.0, sign * 0.36, -sign * 0.36, sign * 0.72, -sign * 0.72, sign * 1.1, -sign * 1.1, sign * 1.55, -sign * 1.55, sign * 2.15, -sign * 2.15, math.pi)
        best_angle = None
        best_score = -float('inf')
        for offset in offsets:
            a = desired + offset
            ux, uy = (math.cos(a), math.sin(a))
            if not self._can_swim_to(self.x + ux * speed, self.y + uy * speed, world):
                continue
            openness = 0
            for lookahead in (0.35, 0.8, 1.45):
                if self._can_swim_to(self.x + ux * lookahead, self.y + uy * lookahead, world):
                    openness += 1
                else:
                    break
            score = 0.65 * math.cos(offset) + 0.65 * openness
            if score > best_score:
                best_score = score
                best_angle = a
        return best_angle

    def update(self, player_x, player_y, world):
        self.active = abs(self.x - player_x) <= VIEW_RADIUS and abs(self.y - player_y) <= VIEW_RADIUS
        if self.hp <= 0:
            return
        dx_player = player_x - self.x
        dy_player = player_y - self.y
        distance_sq = dx_player * dx_player + dy_player * dy_player
        flee = self.active and self.type.behavior == 'prey' and (1e-05 < distance_sq < self.type.fear_distance ** 2)
        if self.fleeing and (not flee):
            self.move_timer = max(12, self.move_timer)
        self.fleeing = flee
        self.swim_phase += self.phase_rate
        self.speed_phase += 0.055
        self.move_timer -= 1
        if self.move_timer <= 0:
            self.cruise_heading += random.uniform(-1.1, 1.1)
            self.move_timer = random.randint(30, 85)
            self.phase_rate = random.uniform(0.075, 0.135)
        detection_range = PREDATOR_DETECTION_RANGE_BY_NAME.get(self.type.name, PREDATOR_DETECTION_RANGE_DEFAULT)
        player_detected = self.active and self.type.behavior == 'predator' and (distance_sq <= detection_range * detection_range)
        if flee:
            target_heading = math.atan2(-dy_player, -dx_player)
            target_heading += math.sin(self.swim_phase * 1.8) * (0.1 + 0.25 * self.type.zigzag_strength)
        elif player_detected and distance_sq > 0.01:
            target_heading = math.atan2(dy_player, dx_player)
        else:
            target_heading = self.cruise_heading + self.type.zigzag_strength * math.sin(self.swim_phase)
        max_turn = 0.48 if flee else 0.28 if self.type.behavior == 'predator' else 0.14
        diff = self._angle_difference(target_heading, self.swim_heading)
        self.swim_heading += max(-max_turn, min(max_turn, diff))
        speed_wave = math.sin(self.speed_phase)
        speed_factor = max(0.55, 1.0 + self.type.speed_variation * speed_wave)
        if flee:
            speed_factor *= self.type.flee_speed_boost * (1.0 + 0.12 * math.sin(self.swim_phase * 2.3))
        speed = max(0.002, self.type.speed * speed_factor)
        heading = self.swim_heading
        nx = self.x + math.cos(heading) * speed
        ny = self.y + math.sin(heading) * speed
        if not self._can_swim_to(nx, ny, world):
            alternate = self._find_open_heading(target_heading, speed, world)
            if alternate is not None:
                heading = alternate
                nx = self.x + math.cos(heading) * speed
                ny = self.y + math.sin(heading) * speed
                self.swim_heading = heading
                if not flee and self.type.behavior != 'predator':
                    self.cruise_heading = heading
                self.move_timer = random.randint(20, 48)
                self.stuck_frames = 0
            else:
                self.stuck_frames += 1
                if self.stuck_frames >= 12:
                    relocated = False
                    for radius in (0.25, 0.65, 1.15, 2.0):
                        for i in range(16):
                            a = i * math.tau / 16.0
                            tx = self.x + math.cos(a) * radius
                            ty = self.y + math.sin(a) * radius
                            if self._can_swim_to(tx, ty, world):
                                self.x, self.y = (tx, ty)
                                self.swim_heading = a
                                self.cruise_heading = a
                                self.move_timer = 35
                                self.stuck_frames = 0
                                relocated = True
                                break
                        if relocated:
                            break
                self.dx = self.dy = 0.0
                self._update_rotation()
                return
        else:
            self.stuck_frames = 0
        self.x, self.y = (nx, ny)
        self.dx = math.cos(heading) * speed
        self.dy = math.sin(heading) * speed
        new_visual = -math.degrees(heading) if self.type.fish_style else math.degrees(heading + math.pi / 2)
        if abs(self._angle_difference(math.radians(new_visual), math.radians(getattr(self, 'visual_angle', 9999.0)))) > 0.045:
            self._update_rotation()

class CaveWorldProxy:

    def __init__(self, object_grid):
        self.object_grid = object_grid
        self.cave_lookup = None

    def get_object_at(self, x, y):
        return self.object_grid.get((int(x), int(y)))

    def remove_object_at(self, x, y):
        return self.object_grid.pop((int(x), int(y)), None)

class NightWaterSpark:
    """Ночной планктон/малёк: только декоративная точка без физики и лута.

    Координаты задаются в клетках мира; крошечные световые ореолы
    формируются уже на экране. Не влияет на спавн полноценных рыб.
    """
    __slots__ = ('x', 'y', 'vx', 'vy', 'phase', 'blink_speed', 'wander', 'jitter', 'radius', 'color', 'fry', 'flee_distance', 'flee_speed')

    def __init__(self, rng, x, y):
        self.x = float(x)
        self.y = float(y)
        self.fry = rng.random() < NIGHT_FRY_CHANCE
        angle = rng.uniform(0.0, math.tau)
        speed = rng.uniform(0.018, 0.065) if self.fry else rng.uniform(0.002, 0.022)
        self.vx = math.cos(angle) * speed
        self.vy = math.sin(angle) * speed
        self.wander = rng.uniform(0.0015, 0.012 if self.fry else 0.006)
        self.phase = rng.uniform(0, math.tau)
        self.blink_speed = rng.uniform(0.06, 0.27)
        self.jitter = rng.uniform(0.3, 1.5 if self.fry else 1.1)
        roll = rng.random()
        if self.fry:
            self.radius = rng.uniform(1.65, 2.45) if roll < 0.9 else rng.uniform(2.55, 3.25)
        else:
            self.radius = rng.uniform(0.65, 1.4) if roll < 0.89 else rng.uniform(1.45, 2.35)
        palette = ((88, 204, 255), (113, 229, 255), (66, 158, 255), (155, 240, 255), (79, 195, 242), (58, 147, 239), (73, 235, 208), (114, 255, 208), (190, 157, 255), (245, 186, 255), (255, 215, 130), (156, 255, 158))
        self.color = palette[rng.randrange(6) if rng.random() < 0.79 else rng.randrange(6, len(palette))]
        if rng.random() < NIGHT_FLEE_CHANCE + (0.1 if self.fry else 0.0):
            self.flee_distance = rng.uniform(1.6, 5.7 if self.fry else 3.9)
            self.flee_speed = rng.uniform(0.065, 0.145) if self.fry else rng.uniform(0.024, 0.065)
        else:
            self.flee_distance = 0.0
            self.flee_speed = 0.0

class Game:

    def __init__(self):
        pygame.init()
        display_flags = pygame.FULLSCREEN
        if hasattr(pygame, 'SCALED'):
            display_flags |= pygame.SCALED
        self.screen = pygame.display.set_mode((SCREEN_SIZE, SCREEN_SIZE), display_flags)
        pygame.display.set_caption('2D Subnautica — каноническая фауна и флора')
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont('Arial', 20)
        self.large_font = pygame.font.SysFont('Arial', 32)
        self.small_font = pygame.font.SysFont('Arial', 15)
        self.tiny_font = pygame.font.SysFont('Arial', 13)
        self.screen.fill((8, 18, 30))
        loading = self.large_font.render('Генерация мира...', True, (200, 230, 255))
        self.screen.blit(loading, loading.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE // 2)))
        pygame.display.flip()
        init_assets()
        self.world = World()
        # Аврора лежит на востоке карты (примерно x=750..850).
        # Выбираем безопасное мелководье к западу, лицом к Авроре.
        candidates = []
        for xx in range(690, 450, -2):
            for yy in range(490, 550, 2):
                biome = self.world.get_biome_at(xx, yy)
                if (biome is not None and biome.name == 'Мелководье'
                        and all(self.world.get_biome_at(xx+dx, yy+dy) is biome for dx in (-4, 4) for dy in (-3, 3))
                        and self.world.is_water_at(xx, yy)):
                    candidates.append((xx, yy))
        if not candidates:
            for xx in range(700, 300, -4):
                for yy in range(420, 620, 4):
                    biome = self.world.get_biome_at(xx, yy)
                    if biome is not None and biome.name == 'Мелководье' and self.world.is_water_at(xx, yy):
                        candidates.append((xx, yy))
        self.player_x, self.player_y = (candidates[0] if candidates else (MAP_SIZE // 2, MAP_SIZE // 2))
        capsule_x1 = int(self.player_x) - START_CAPSULE_WIDTH // 2
        capsule_y1 = int(self.player_y) - START_CAPSULE_HEIGHT // 2
        capsule_x2 = capsule_x1 + START_CAPSULE_WIDTH - 1
        capsule_y2 = capsule_y1 + START_CAPSULE_HEIGHT - 1
        self.start_capsule_rect = (capsule_x1, capsule_y1, capsule_x2, capsule_y2)
        self.start_capsule_inner_rect = (capsule_x1 + 1, capsule_y1 + 1, capsule_x2 - 1, capsule_y2 - 1)
        door_y = capsule_y2
        door_x1 = capsule_x1 + START_CAPSULE_WIDTH // 2 - 1
        self.start_capsule_door_cells = {(door_x1, door_y), (door_x1 + 1, door_y)}
        for cy in range(capsule_y1, capsule_y2 + 1):
            for cx in range(capsule_x1, capsule_x2 + 1):
                self.world.object_grid.pop((cx, cy), None)
                self.world.flora_grid.pop((cx, cy), None)
        self.world.animals = [animal for animal in self.world.animals if not (capsule_x1 <= int(animal.x) <= capsule_x2 and capsule_y1 <= int(animal.y) <= capsule_y2)]
        self.world.rebuild_animal_index()
        self.world.creature_blocked = self.is_start_capsule_wall
        # Старт игрока в центре каюты №6 Авроры. Координаты помещений
        # смещены при построении карты на AURORA_SHIFT_X = 550.
        self.player_x, self.player_y = 793.0, 635.0
        # Убираем возможный предмет из стартовой клетки.
        self.world.object_grid.pop((int(self.player_x), int(self.player_y)), None)
        self.in_cave = False
        self.cave_tunnels = []
        self.cave_animals = []
        self.cave_object_grid = {}
        self.cave_flora_grid = {}
        self.cave_world = CaveWorldProxy(self.cave_object_grid)
        self.cave_world.cave_lookup = self.get_cave_tunnel_at
        self.cave_entrances = []
        self.cave_tunnel_chunks = {}
        self.cave_tunnel_cache = {}
        self.surface_cave_entrance_cells = {}
        self.cave_exit_cells = {}
        self._init_caves()
        # Счётчики предметов/флоры поддерживаются при подборе и респавне:
        # щелчок по биому больше не сканирует всю карту.
        self._install_biome_map_index()
        self.hostile_projectiles = []
        self.special_explosions = []
        self.plant_attack_timers = {}
        self.running = True
        self.state = 'playing'
        self.settings_open = False
        self.setting_plankton_enabled = NIGHT_PLANKTON_ENABLED
        self.setting_seabed_details_enabled = True
        self.setting_water_darkening_enabled = True
        # Выключатель всех NPC в настройках: одиночный режим.
        # Состояние бригады сохраняется при включении/выключении в рамках запуска игры.
        self.setting_workers_enabled = True
        self.player_hp = 25
        self.max_player_hp = 25
        self.player_attack_damage = 5
        self.player_attack_range = 1.5
        self.max_hunger = 100
        self.max_thirst = 100
        self.player_hunger = self.max_hunger
        self.player_thirst = self.max_thirst
        self.hunger_timer = 0
        self.thirst_timer = 0
        self.equipped_weapon = None
        self.cooldown_animals = set()
        self.mouse_world_x = 0
        self.mouse_world_y = 0
        self.player_angle = 0.0
        self.inventory = [None] * INVENTORY_SIZE
        self.inventory_counts = {}
        self.active_slot_index = -1
        self._stored_companion_hp = {}
        self._load_start_inventory()
        self.pet_list_page = 0
        self.pets_per_page = 9
        self.selected_pet = None
        self.surface_dropped_items = {}
        self.cave_dropped_items = {}
        self.craft_page = 0
        self.craft_station_index = 0
        self.recipes_per_page = 9
        self.max_craft_page = max(0, (len(recipes) - 1) // self.recipes_per_page)
        self.notification = ''
        self.notification_timer = 0
        self.flashlight_on = False
        self.vehicle_headlights_on = VEHICLE_HEADLIGHTS_DEFAULT
        self.seaglide_on = False
        self.active_flare_timer = 0
        self.speed_boost_timer = 0
        self.damage_resist_timer = 0
        self.damage_vignette_flash = 0
        self.damage_vignette_surface = None
        self.scanner_highlight_timer = 0
        self.scanner_highlight_cells = set()
        self.scanned_entries = set()
        self.researched_flora = set()
        self.scanner_target = None
        self.scanner_scroll_offset = 0
        self.scanner_scroll_max = 0
        self.scanner_preview_cache = {}
        self.planted_flora = {}
        self.planting_target = None
        self.planting_page = 0
        self.plants_per_page = 9
        self.active_decoys = []
        self.base_effect_tick = 0
        self.diver_sprite = generate_diver_sprite(size=CELL_SIZE, suit_color=(30, 100, 180), accent_color=(255, 200, 50), tank_color=(200, 205, 215))
        self.walker_sprites = (generate_walker_sprite(CELL_SIZE, -1), generate_walker_sprite(CELL_SIZE, 0), generate_walker_sprite(CELL_SIZE, 1))
        self.player_mode = 'swimming'
        self.walk_animation = 0
        self.diver_rotated_cache = {}
        self.weapon_rotated_cache = {}
        self.vehicle_rotated_cache = {}
        self.inventory_image_cache = {}
        self.tree_sprite_cache = {}
        self.active_vehicle = None
        self.vehicle_max_hp = {seamoth_type: 50, cyclops_type: 400}
        self.vehicle_hp = dict(self.vehicle_max_hp)
        self.vehicle_wrecks = []
        self.vehicle_torpedoes = []
        self.vehicle_torpedo_cooldown = 0
        self.inventory_overlay = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA).convert_alpha()
        self.inventory_overlay.fill((0, 0, 0, 180))
        self.crafting_overlay = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA).convert_alpha()
        self.crafting_overlay.fill((0, 0, 0, 200))
        self.camera_zoom = 1.0
        self._render_view_radius = VIEW_RADIUS
        self._render_canvas_size = SCREEN_SIZE
        self._world_render_surface = None
        self._blue_edge_haze = self._make_blue_edge_haze()
        self._night_edge_vignette = self._make_night_edge_vignette()
        self._hd_source_cache = {}
        self._hd_glow_source_cache = {}
        self._hd_sprite_cache = OrderedDict()
        self._hd_floor_cache = OrderedDict()
        self.map_zoom = 1.0
        self.map_min_zoom = 0.3
        self.map_max_zoom = 20.0
        self.map_base_scale = SCREEN_SIZE / MAP_SIZE
        self.map_center_x = self.player_x
        self.map_center_y = self.player_y
        self.selected_biome = None
        self.map_panel_scroll = 0
        self._map_biome_species_cache = None
        self.whale_quests = []
        self.whale_pending_punishment = False
        self.whale_punishment_remaining = 0
        self.whale_quest_serial = 0
        self.whale_popup_queue = []
        self.whale_quest_panel_page = 0
        pass
        self.day_time = 0.3
        self.day_cycle_seconds = 180.0
        self.day_speed = 1.0 / (self.day_cycle_seconds * 60.0)
        self.elapsed_game_days = 0.0
        self.ecosystem_refresh_interval_days = 4.0
        self.next_ecosystem_refresh_day = self.ecosystem_refresh_interval_days
        self._capture_ecosystem_targets()
        self.day_overlay = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE)).convert()
        self.lighting_overlay = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA).convert_alpha()
        self._radial_light_cache = {}
        self._beam_light_cache = {}
        self._emitter_glow_cache = {}
        self._day_overlay_key = None
        self.frame_count = 0
        self.force_midnight_preview = False
        self._bio_planted_scaled_cache = {}
        self._night_rng = random.Random(56021026)
        self._night_sparks = []
        self._night_sparks_visible = 0
        self._night_spark_surface = None
        self._seabed_tile_cache = {}
        self._seabed_theme_cache = {}
        self._cave_floor_tile_cache = {}
        self._cave_wall_tile_cache = {}
        self._cave_theme_cache = {}
        pygame.mouse.set_visible(True)

    @staticmethod
    def _increment_target(counter, key, amount=1):
        counter[key] = counter.get(key, 0) + amount

    def _capture_ecosystem_targets(self):
        """Запоминает исходную численность ресурсов и дикой фауны.

        Целевые значения снимаются уже после создания стартовой капсулы и
        пещер, поэтому восстановление не пытается вернуть то, что генератор
        намеренно убрал из стартовой зоны. Питомцы в целевую популяцию не
        входят: приручение существа уменьшает дикую популяцию, и позже
        недостающий дикий экземпляр может восстановиться.
        """
        self.surface_resource_targets = {}
        self.cave_resource_targets = {}
        self.surface_fauna_targets = {}
        self.cave_fauna_targets = {}
        for (x, y), obj in self.world.object_grid.items():
            if getattr(obj, 'is_buildable', False) or getattr(obj, 'is_weapon', False):
                continue
            biome = self.world.get_biome_at(x, y)
            if biome is None:
                continue
            self._increment_target(self.surface_resource_targets, (biome, obj))
        for (x, y), obj in self.cave_object_grid.items():
            if getattr(obj, 'is_buildable', False):
                continue
            tunnel = self.get_cave_tunnel_at(x, y)
            if tunnel is None:
                continue
            self._increment_target(self.cave_resource_targets, (tunnel, obj))
        for animal in self.world.animals:
            if getattr(animal, 'is_companion', False):
                continue
            home = getattr(animal, 'home_biome', None)
            if home is None:
                continue
            self._increment_target(self.surface_fauna_targets, (home, animal.type))
        for animal in self.cave_animals:
            if getattr(animal, 'is_companion', False):
                continue
            home = getattr(animal, 'home_biome', None)
            if home is None:
                continue
            self._increment_target(self.cave_fauna_targets, (home, animal.type))

    def _surface_cell_in_start_capsule(self, x, y):
        x1, y1, x2, y2 = self.start_capsule_rect
        return x1 <= x <= x2 and y1 <= y <= y2

    def _spawn_missing_surface_resource(self, biome, object_type):
        """Пытается разместить один ресурс в случайной точке его биома."""
        for _attempt in range(160):
            rx, ry = biome.random_point()
            x = int(rx)
            y = int(ry)
            key = (x, y)
            if self.world.get_biome_at(x, y) is not biome:
                continue
            if not biome.passable:
                continue
            if self._surface_cell_in_start_capsule(x, y):
                continue
            if key in self.surface_cave_entrance_cells:
                continue
            if self.world.get_object_at(x, y) is not None:
                continue
            if self.surface_dropped_items.get(key):
                continue
            self.world.object_grid[key] = object_type
            return True
        return False

    def _spawn_missing_cave_resource(self, tunnel, object_type):
        """Пытается разместить один ресурс в случайной точке пещерной секции."""
        for _attempt in range(160):
            rx, ry = tunnel.random_point()
            x = int(rx)
            y = int(ry)
            key = (x, y)
            if self.get_cave_tunnel_at(x, y) is not tunnel:
                continue
            if key in self.cave_exit_cells:
                continue
            if key in self.cave_object_grid:
                continue
            if self.cave_dropped_items.get(key):
                continue
            self.cave_object_grid[key] = object_type
            return True
        return False

    def _spawn_missing_surface_animal(self, biome, animal_type):
        """Возвращает одну дикую рыбу/существо в случайную точку биома."""
        for _attempt in range(120):
            x, y = biome.random_point()
            if self.world.get_biome_at(int(x), int(y)) is not biome:
                continue
            if self._surface_cell_in_start_capsule(int(x), int(y)):
                continue
            self.world.animals.append(Animal(x, y, animal_type, biome))
            return True
        return False

    def _spawn_missing_cave_animal(self, tunnel, animal_type):
        """Возвращает одно дикое существо в случайную точку пещерной секции."""
        for _attempt in range(120):
            x, y = tunnel.random_point()
            if self.get_cave_tunnel_at(int(x), int(y)) is not tunnel:
                continue
            self.cave_animals.append(Animal(x, y, animal_type, tunnel))
            return True
        return False

    def _count_current_surface_resources(self):
        counts = {}
        for (x, y), obj in self.world.object_grid.items():
            if getattr(obj, 'is_buildable', False):
                continue
            biome = self.world.get_biome_at(x, y)
            key = (biome, obj)
            if key in self.surface_resource_targets:
                self._increment_target(counts, key)
        for (x, y), stack in self.surface_dropped_items.items():
            biome = self.world.get_biome_at(x, y)
            for item in stack:
                key = (biome, item)
                if key in self.surface_resource_targets:
                    self._increment_target(counts, key)
        return counts

    def _count_current_cave_resources(self):
        counts = {}
        for (x, y), obj in self.cave_object_grid.items():
            if getattr(obj, 'is_buildable', False):
                continue
            tunnel = self.get_cave_tunnel_at(x, y)
            key = (tunnel, obj)
            if key in self.cave_resource_targets:
                self._increment_target(counts, key)
        for (x, y), stack in self.cave_dropped_items.items():
            tunnel = self.get_cave_tunnel_at(x, y)
            for item in stack:
                key = (tunnel, item)
                if key in self.cave_resource_targets:
                    self._increment_target(counts, key)
        return counts

    def _count_current_fauna(self, animals, targets):
        counts = {}
        for animal in animals:
            if getattr(animal, 'is_companion', False):
                continue
            home = getattr(animal, 'home_biome', None)
            key = (home, animal.type)
            if key in targets:
                self._increment_target(counts, key)
        return counts

    def refresh_ecosystem(self):
        """Восполняет недостающую фауну и ресурсы до стартовых значений."""
        restored_resources = 0
        restored_fauna = 0
        surface_resource_counts = self._count_current_surface_resources()
        cave_resource_counts = self._count_current_cave_resources()
        for (biome, object_type), target_count in self.surface_resource_targets.items():
            missing = max(0, target_count - surface_resource_counts.get((biome, object_type), 0))
            for _ in range(missing):
                if self._spawn_missing_surface_resource(biome, object_type):
                    restored_resources += 1
        for (tunnel, object_type), target_count in self.cave_resource_targets.items():
            missing = max(0, target_count - cave_resource_counts.get((tunnel, object_type), 0))
            for _ in range(missing):
                if self._spawn_missing_cave_resource(tunnel, object_type):
                    restored_resources += 1
        surface_fauna_counts = self._count_current_fauna(self.world.animals, self.surface_fauna_targets)
        cave_fauna_counts = self._count_current_fauna(self.cave_animals, self.cave_fauna_targets)
        surface_added = False
        for (biome, animal_type), target_count in self.surface_fauna_targets.items():
            missing = max(0, target_count - surface_fauna_counts.get((biome, animal_type), 0))
            for _ in range(missing):
                if self._spawn_missing_surface_animal(biome, animal_type):
                    restored_fauna += 1
                    surface_added = True
        for (tunnel, animal_type), target_count in self.cave_fauna_targets.items():
            missing = max(0, target_count - cave_fauna_counts.get((tunnel, animal_type), 0))
            for _ in range(missing):
                if self._spawn_missing_cave_animal(tunnel, animal_type):
                    restored_fauna += 1
        if surface_added:
            self.world.rebuild_animal_index()
        refresh_day = int(round(self.next_ecosystem_refresh_day))
        if restored_fauna or restored_resources:
            self.notification = f'Обновление экосистемы — день {refresh_day}: восстановлено рыб/существ {restored_fauna}, ресурсов {restored_resources}'
        else:
            self.notification = f'Обновление экосистемы — день {refresh_day}: пополнение не требуется'
        self.notification_timer = 240

    def _whale_quest_spec(self, kind):
        specs = {'fasting': (20, 'НЕ ЕСТЬ 20 ЧАСОВ', 'Не ешьте 20 игровых часов; пить воду разрешено.'), 'marathon': (3, 'ПРОПЛЫТЬ 200 КЛЕТОК', 'За 3 игровых часа проплывите 200 клеток без транспорта.'), 'empty_inventory': (4, 'ВЫБРОСИТЬ ВЕСЬ ИНВЕНТАРЬ', 'За 4 игровых часа выбросьте P все вещи и существа. Инвентарь должен стать пустым.'), 'whale_hunt': (4, 'УНИЧТОЖИТЬ ЛЮБОГО ЧБОС-КИТА', 'За 4 игровых часа уничтожьте одного чёрно-бело-оранжево-синего кита.'), 'fauna_catalogue': (48, 'СОБРАТЬ ВСЮ ФАУНУ', 'За 2 игровых дня положите в инвентарь каждый обитающий в мире вид фауны хотя бы раз.')}
        return specs[kind]

    def _whale_visible_remaining(self, quest):
        hours = max(0.0, (quest['deadline_day'] - self.elapsed_game_days) * 24)
        return '%02d:%02d' % (int(hours), min(59, int(hours % 1 * 60)))

    def _whale_quest_progress(self, quest):
        kind = quest['kind']
        if kind == 'marathon':
            return 'Проплыто: %d/200 клеток' % int(quest['swum'])
        if kind == 'fauna_catalogue':
            seen, target = (quest['seen_fauna'], quest['fauna_target'])
            return 'Найдено видов: %d/%d' % (len(seen & target), len(target))
        if kind == 'empty_inventory':
            return 'Предметов осталось: %d' % sum((item is not None for item in self.inventory))
        if kind == 'fasting':
            return 'Еда запрещена, вода разрешена'
        if kind == 'whale_hunt':
            return 'Уничтожено китов: 0/1'
        return 'Выполните указанное задание'

    def _queue_whale_popup(self, heading, quest, extra=''):
        hours = quest['duration_hours']
        duration = '2 игровых дня' if hours == 48 else '%d игровых часов' % hours
        self.whale_popup_queue.append({'heading': heading, 'lines': [quest['title'], quest['instruction'], 'Срок задания: ' + duration, extra or self._whale_quest_progress(quest), 'ПРОВАЛ = 20 диких драконов-левиафанов в воде!'], 'until': None})
        if len(self.whale_popup_queue) > 14:
            self.whale_popup_queue = self.whale_popup_queue[-14:]

    def _start_whale_quest(self, kind=None):
        """Волна навязывает 1-2 разных задания; действующие не заменяет."""
        pool = ('fasting', 'marathon', 'empty_inventory', 'whale_hunt', 'fauna_catalogue')
        available = [item for item in pool if not any((q['kind'] == item for q in self.whale_quests))]
        if kind is not None:
            available = [kind] if kind in available and kind in pool else []
        if not available:
            return
        how_many = 1 if kind is not None else min(len(available), 2 if random.random() < 0.4 else 1)
        for quest_kind in random.sample(available, how_many):
            hours, title, instruction = self._whale_quest_spec(quest_kind)
            self.whale_quest_serial += 1
            quest = {'id': self.whale_quest_serial, 'kind': quest_kind, 'title': title, 'instruction': instruction, 'start_day': self.elapsed_game_days, 'deadline_day': self.elapsed_game_days + hours / 24, 'duration_hours': hours, 'swum': 0.0, 'seen_fauna': set(), 'fauna_target': frozenset((a.type.name for a in self.world.animals + self.cave_animals if not getattr(a, 'is_companion', False))), 'dropped_any': False}
            self.whale_quests.append(quest)
            self._queue_whale_popup('НОВЫЙ ОБЯЗАТЕЛЬНЫЙ КВЕСТ!', quest)
            if quest_kind == 'empty_inventory' and (not any(self.inventory)):
                self._finish_whale_quest(quest)
        self.notification = 'ЧБОС-КИТ: НОВЫЕ КВЕСТЫ! СМОТРИТЕ ИНВЕНТАРЬ (R)'
        self.notification_timer = 180

    def _finish_whale_quest(self, quest):
        if quest not in self.whale_quests:
            return
        self.whale_quests.remove(quest)
        self._queue_whale_popup('КВЕСТ ВЫПОЛНЕН!', quest, 'Требование выполнено. Вы пережили кита!')
        self.notification = 'ЧБОС-КИТ: КВЕСТ ВЫПОЛНЕН!'
        self.notification_timer = 150

    def _fail_whale_quest(self, quest, reason):
        if quest not in self.whale_quests:
            return
        self.whale_quests.remove(quest)
        self.whale_pending_punishment = True
        self.whale_punishment_remaining += WHALE_PUNISHMENT_DRAGONS
        self._queue_whale_popup('КВЕСТ ПРОВАЛЕН!', quest, 'Причина: ' + reason)
        self.notification = 'ЧБОС-КИТ: ПРОВАЛ! +20 ДРАКОНОВ!'
        self.notification_timer = 200

    def _whale_quest_on_food(self):
        for quest in list(self.whale_quests):
            if quest['kind'] == 'fasting':
                self._fail_whale_quest(quest, 'вы поели')


    def _whale_quest_on_catch(self, animal_type, slot_index):
        if isinstance(animal_type, AnimalType):
            for quest in list(self.whale_quests):
                if quest['kind'] == 'fauna_catalogue' and animal_type.name in quest['fauna_target']:
                    quest['seen_fauna'].add(animal_type.name)
                    if quest['seen_fauna'] >= quest['fauna_target']:
                        self._finish_whale_quest(quest)


    def _whale_quest_on_drop(self):
        for quest in list(self.whale_quests):
            if quest['kind'] == 'empty_inventory':
                quest['dropped_any'] = True
                if not any(self.inventory):
                    self._finish_whale_quest(quest)

    def _whale_quest_on_whale_death(self, animal):
        if animal.type.name == WHALE_NAME and animal.hp <= 0 and (not getattr(animal, '_whale_kill_recorded', False)):
            animal._whale_kill_recorded = True
            for quest in list(self.whale_quests):
                if quest['kind'] == 'whale_hunt':
                    self._finish_whale_quest(quest)

    def _update_whale_quest(self, old_x, old_y):
        for quest in list(self.whale_quests):
            if quest['kind'] == 'marathon' and (not self._is_player_on_land()) and (self.active_vehicle is None):
                step = math.hypot(self.player_x - old_x, self.player_y - old_y)
                if step <= 1.5:
                    quest['swum'] += step
                if quest['swum'] >= 200:
                    self._finish_whale_quest(quest)
                    continue
            if self.elapsed_game_days >= quest['deadline_day']:
                if quest['kind'] == 'fasting':
                    self._finish_whale_quest(quest)
                else:
                    self._fail_whale_quest(quest, 'время истекло')
        if self.whale_pending_punishment:
            self._apply_whale_punishment()

    def _apply_whale_punishment(self):
        if not self.whale_pending_punishment or self._is_player_on_land():
            return
        created = 0
        rng = random.Random(pygame.time.get_ticks() + self.whale_quest_serial * 1007)
        used_cells = set()
        remaining = self.whale_punishment_remaining
        for i in range(remaining):
            chosen = None
            for attempt in range(500):
                r = rng.uniform(2.0, 8.5) if attempt < 400 else rng.uniform(0.5, 12.0)
                a = rng.uniform(0.0, math.tau)
                x = self.player_x + math.cos(a) * r
                y = self.player_y + math.sin(a) * r
                if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
                    continue
                if (int(x), int(y)) in used_cells:
                    continue
                if self.in_cave:
                    home = self.get_cave_tunnel_at(x, y)
                    passable = home is not None and self.cave_is_passable(x, y)
                else:
                    home = self.world.get_biome_at(x, y)
                    obj = self.world.get_object_at(int(x), int(y))
                    passable = home is not None and (not is_land_biome(home)) and (obj is None or not obj.is_wall) and (not self.is_start_capsule_wall(int(x), int(y)))
                if passable:
                    chosen = (x, y, home)
                    break
            if chosen is None:
                break
            x, y, home = chosen
            used_cells.add((int(x), int(y)))
            dragon = Animal(x, y, sea_dragon_type, home)
            if self.in_cave:
                self.cave_animals.append(dragon)
            else:
                self.world.animals.append(dragon)
            created += 1
        if created and (not self.in_cave):
            self.world.rebuild_animal_index()
        self.whale_punishment_remaining -= created
        if self.whale_punishment_remaining <= 0:
            self.whale_punishment_remaining = 0
            self.whale_pending_punishment = False
            self.notification = 'НАКАЗАНИЕ! 20 ДРАКОНОВ!'
            self.notification_timer = 180

    def _whale_wrap_text(self, text, font, max_width):
        """Переносит русские строки по ширине колонки без обрезания."""
        result = []
        for paragraph in str(text).split('\n'):
            line = ''
            for word in paragraph.split():
                candidate = (line + ' ' + word).strip()
                if font.size(candidate)[0] <= max_width:
                    line = candidate
                else:
                    if line:
                        result.append(line)
                    line = word
                    while line and font.size(line)[0] > max_width:
                        cutoff = max(1, len(line) - 1)
                        while cutoff > 1 and font.size(line[:cutoff])[0] > max_width:
                            cutoff -= 1
                        result.append(line[:cutoff])
                        line = line[cutoff:]
            result.append(line)
        return result

    def _draw_whale_quest_hud(self):
        if self.state != 'playing':
            return
        count = len(self.whale_quests)
        if not count and (not self.whale_pending_punishment):
            return
        status = 'КВЕСТЫ КИТА: %d  |  R: СПИСОК' % count
        if self.whale_pending_punishment:
            status += '  |  ДРАКОНОВ: %d' % self.whale_punishment_remaining
        rect = pygame.Rect(153, 53, SCREEN_SIZE - 165, 31)
        pygame.draw.rect(self.screen, (20, 32, 44), rect, border_radius=6)
        pygame.draw.rect(self.screen, (246, 149, 45), rect, 2, border_radius=6)
        self.screen.blit(self.tiny_font.render(status, True, (255, 206, 112)), (160, 61))

    def _draw_whale_quest_popup(self):
        if self.state != 'playing' or self.settings_open or (not self.whale_popup_queue):
            return
        now = pygame.time.get_ticks()
        popup = self.whale_popup_queue[0]
        if popup['until'] is None:
            popup['until'] = now + 6000
        if now >= popup['until']:
            self.whale_popup_queue.pop(0)
            return
        panel = pygame.Rect(65, 171, SCREEN_SIZE - 130, 240)
        pygame.draw.rect(self.screen, (16, 28, 41), panel, border_radius=10)
        pygame.draw.rect(self.screen, (252, 156, 38), panel, 3, border_radius=10)
        header = self.small_font.render(popup['heading'], True, (255, 187, 93))
        self.screen.blit(header, (panel.x + 15, panel.y + 12))
        y = panel.y + 45
        for i, line in enumerate(popup['lines']):
            color = (255, 224, 140) if i == 0 else (232, 240, 242)
            if i == len(popup['lines']) - 1:
                color = (255, 125, 111)
            for wrapped in self._whale_wrap_text(line, self.small_font, panel.width - 30):
                if y >= panel.bottom - 23:
                    break
                self.screen.blit(self.small_font.render(wrapped, True, color), (panel.x + 15, y))
                y += 21
            y += 6

    def _inventory_grid_layout(self):
        grid_width = INVENTORY_COLS * (SLOT_SIZE + SLOT_MARGIN) - SLOT_MARGIN
        grid_height = INVENTORY_ROWS * (SLOT_SIZE + SLOT_MARGIN) - SLOT_MARGIN
        return (25, (SCREEN_SIZE - grid_height) // 2, grid_width, grid_height)

    def _draw_whale_quest_inventory_column(self):
        x, y, w, h = (390, 112, SCREEN_SIZE - 405, 445)
        panel = pygame.Rect(x, y, w, h)
        pygame.draw.rect(self.screen, (20, 30, 42), panel, border_radius=8)
        pygame.draw.rect(self.screen, (234, 138, 48), panel, 2, border_radius=8)
        self.screen.blit(self.tiny_font.render('ЗАДАНИЯ ЧБОС-КИТА', True, (255, 183, 91)), (x + 10, y + 12))
        quests_per_page = 2
        pages = max(1, (len(self.whale_quests) + quests_per_page - 1) // quests_per_page)
        self.whale_quest_panel_page = min(self.whale_quest_panel_page, pages - 1)
        start = self.whale_quest_panel_page * quests_per_page
        selected = self.whale_quests[start:start + quests_per_page]
        if not selected:
            self.screen.blit(self.tiny_font.render('Активных квестов нет', True, (183, 213, 208)), (x + 12, y + 55))
        for i, quest in enumerate(selected):
            top = y + 44 + i * 176
            if i:
                pygame.draw.line(self.screen, (93, 94, 103), (x + 9, top - 7), (x + w - 9, top - 7))
            max_line_width = w - 22
            yy = top
            title_lines = self._whale_wrap_text(quest['title'], self.tiny_font, max_line_width)[:2]
            for line in title_lines:
                self.screen.blit(self.tiny_font.render(line, True, (255, 196, 106)), (x + 10, yy))
                yy += 17
            info = self._whale_wrap_text(quest['instruction'], self.tiny_font, max_line_width)
            for line in info[:4]:
                self.screen.blit(self.tiny_font.render(line, True, (222, 226, 236)), (x + 10, yy))
                yy += 15
            progress_lines = self._whale_wrap_text(self._whale_quest_progress(quest), self.tiny_font, max_line_width)
            for line in progress_lines[:2]:
                self.screen.blit(self.tiny_font.render(line, True, (150, 232, 220)), (x + 10, yy))
                yy += 15
            self.screen.blit(self.tiny_font.render('ОСТАЛОСЬ: ' + self._whale_visible_remaining(quest), True, (255, 167, 116)), (x + 10, max(top + 142, yy + 2)))
        if self.whale_pending_punishment:
            self.screen.blit(self.tiny_font.render('Наказание: %d драконов' % self.whale_punishment_remaining, True, (255, 110, 90)), (x + 10, y + h - 43))
        self.screen.blit(self.tiny_font.render('PgUp/PgDn: %d/%d' % (self.whale_quest_panel_page + 1, pages), True, (180, 200, 221)), (x + 10, y + h - 23))

    def _is_player_on_land(self):
        return not self.in_cave and is_land_biome(self.world.get_biome_at(self.player_x, self.player_y))

    def get_diver_rotated(self, angle):
        angle = quantize_angle(angle)
        walking = self._is_player_on_land()
        self.player_mode = 'walking' if walking else 'swimming'
        stride = (1 if self.walk_animation // 8 % 2 else -1) if walking and self.walk_animation > 0 else 0
        cache_key = ('walk', stride, angle) if walking else ('swim', angle)
        image = self.diver_rotated_cache.get(cache_key)
        if image is None:
            source = self.walker_sprites[stride + 1] if walking else self.diver_sprite
            image = pygame.transform.rotate(source, -angle)
            self.diver_rotated_cache[cache_key] = image
        return image

    def get_weapon_rotated(self, weapon, angle):
        if weapon is None or weapon.image is None:
            return None
        angle = quantize_angle(angle)
        key = (id(weapon), angle)
        image = self.weapon_rotated_cache.get(key)
        if image is None:
            image = pygame.transform.rotate(weapon.image, -angle)
            if len(self.weapon_rotated_cache) >= 720:
                self.weapon_rotated_cache.clear()
            self.weapon_rotated_cache[key] = image
        return image

    def get_vehicle_rotated(self, vehicle_type, angle):
        if vehicle_type is None or vehicle_type.image is None:
            return None
        angle = quantize_angle(angle)
        key = (id(vehicle_type), angle)
        image = self.vehicle_rotated_cache.get(key)
        if image is None:
            image = pygame.transform.rotate(vehicle_type.image, -angle)
            if len(self.vehicle_rotated_cache) >= 360:
                self.vehicle_rotated_cache.clear()
            self.vehicle_rotated_cache[key] = image
        return image

    def _remove_inventory_items_of_type(self, item_type, amount=1):
        amount = max(0, int(amount))
        if amount == 0:
            return True
        if self.inventory_counts.get(item_type, 0) < amount:
            return False
        remaining = amount
        for index, item in enumerate(self.inventory):
            if remaining <= 0:
                break
            if item is item_type:
                self.inventory[index] = None
                self._stored_companion_hp.pop(index, None)
                if index == self.active_slot_index:
                    self.active_slot_index = -1
                remaining -= 1
        new_count = self.inventory_counts.get(item_type, 0) - amount
        if new_count > 0:
            self.inventory_counts[item_type] = new_count
        else:
            self.inventory_counts.pop(item_type, None)
        return remaining == 0

    def _nearest_vehicle_wreck(self, radius=1.7):
        best = None
        radius_sq = radius * radius
        for wreck in self.vehicle_wrecks:
            if wreck['in_cave'] != self.in_cave:
                continue
            dx = wreck['x'] - self.player_x
            dy = wreck['y'] - self.player_y
            dist_sq = dx * dx + dy * dy
            if dist_sq <= radius_sq and (best is None or dist_sq < best[0]):
                best = (dist_sq, wreck)
        return best[1] if best is not None else None

    def _preferred_inventory_vehicle(self):
        if 0 <= self.active_slot_index < len(self.inventory):
            selected = self.inventory[self.active_slot_index]
            if selected in self.vehicle_max_hp:
                return selected
        for item in self.inventory:
            if item in self.vehicle_max_hp:
                return item
        return None

    def handle_vehicle_action(self):
        if self.state not in ('playing', 'inventory'):
            return
        if self.active_vehicle is not None:
            name = self.active_vehicle.name
            self.active_vehicle = None
            self.state = 'playing'
            self.notification = f'Вы покинули: {name}'
            self.notification_timer = 60
            return
        if self._is_player_on_land():
            self.notification = 'Подлодки работают только в воде. Вернитесь к берегу.'
            self.notification_timer = 100
            return
        wreck = self._nearest_vehicle_wreck()
        if wreck is not None:
            vehicle_type = wreck['type']
            kits_needed = 2 if vehicle_type is cyclops_type else 1
            kits_have = self.inventory_counts.get(repair_kit, 0)
            if kits_have < kits_needed:
                self.notification = f'Для ремонта {vehicle_type.name} нужно ремкомплектов: {kits_needed} (есть {kits_have})'
                self.notification_timer = 100
                return
            if not self._remove_inventory_items_of_type(repair_kit, kits_needed):
                return
            if not self.add_to_inventory(vehicle_type):
                for _ in range(kits_needed):
                    self.add_to_inventory(repair_kit)
                self.notification = 'Нет места для восстановленного транспорта'
                self.notification_timer = 90
                return
            self.vehicle_hp[vehicle_type] = self.vehicle_max_hp[vehicle_type]
            self.vehicle_wrecks.remove(wreck)
            self.active_vehicle = vehicle_type
            self.state = 'playing'
            self.notification = f'{vehicle_type.name} восстановлен за {kits_needed} ремкомплект(а) и снова готов к работе'
            self.notification_timer = 120
            return
        vehicle_type = self._preferred_inventory_vehicle()
        if vehicle_type is None:
            self.notification = 'В инвентаре нет Мотылька или Циклопа'
            self.notification_timer = 75
            return
        if self.vehicle_hp.get(vehicle_type, 0) <= 0:
            self.notification = f'{vehicle_type.name} уничтожен. Вернитесь к месту крушения для ремонта'
            self.notification_timer = 90
            return
        self.active_vehicle = vehicle_type
        self.state = 'playing'
        self.notification = f'Управление: {vehicle_type.name} | WASD — движение | ЛКМ — торпеда | F — выйти'
        self.notification_timer = 120

    def vehicle_take_damage(self, damage):
        if self.active_vehicle is None:
            self.player_take_damage(damage)
            return
        vehicle_type = self.active_vehicle
        damage = max(1, int(round(damage)))
        self.vehicle_hp[vehicle_type] = max(0, self.vehicle_hp.get(vehicle_type, self.vehicle_max_hp[vehicle_type]) - damage)
        hp = self.vehicle_hp[vehicle_type]
        self.notification = f'{vehicle_type.name}: -{damage} HP, осталось {hp}'
        self.notification_timer = 45
        if hp <= 0:
            self._destroy_active_vehicle()

    def _destroy_active_vehicle(self):
        vehicle_type = self.active_vehicle
        if vehicle_type is None:
            return
        self.vehicle_wrecks.append({'type': vehicle_type, 'x': float(self.player_x), 'y': float(self.player_y), 'in_cave': bool(self.in_cave), 'angle': float(self.player_angle)})
        self._remove_inventory_items_of_type(vehicle_type, 1)
        self.vehicle_hp[vehicle_type] = 0
        self.active_vehicle = None
        kits_needed = 2 if vehicle_type is cyclops_type else 1
        self.notification = f'{vehicle_type.name} уничтожен! Игрок не получил урона. Вернитесь сюда с ремкомплектами: {kits_needed}'
        self.notification_timer = 180

    def fire_vehicle_torpedo(self, target_x, target_y):
        vehicle_type = self.active_vehicle
        if vehicle_type is None or self.state != 'playing':
            return
        if self.vehicle_torpedo_cooldown > 0:
            return
        dx = target_x - self.player_x
        dy = target_y - self.player_y
        length = math.hypot(dx, dy)
        if length <= 0.001:
            radians = math.radians(self.player_angle)
            dx, dy = (math.cos(radians), math.sin(radians))
        else:
            dx /= length
            dy /= length
        if vehicle_type is cyclops_type:
            speed, damage, cooldown = (0.34, 45, 26)
        else:
            speed, damage, cooldown = (0.44, 30, 18)
        self.vehicle_torpedoes.append({'x': self.player_x + dx * 0.9, 'y': self.player_y + dy * 0.9, 'vx': dx * speed, 'vy': dy * speed, 'damage': damage, 'ttl': 85, 'in_cave': bool(self.in_cave)})
        self.vehicle_torpedo_cooldown = cooldown

    def _update_vehicle_torpedoes(self):
        if self.vehicle_torpedo_cooldown > 0:
            self.vehicle_torpedo_cooldown -= 1
        if not self.vehicle_torpedoes:
            return
        survivors = []
        current_animals = self.get_current_animals()
        for torpedo in self.vehicle_torpedoes:
            if torpedo['in_cave'] != self.in_cave:
                continue
            torpedo['x'] += torpedo['vx']
            torpedo['y'] += torpedo['vy']
            torpedo['ttl'] -= 1
            if torpedo['ttl'] <= 0:
                continue
            if not (0 <= torpedo['x'] < MAP_SIZE and 0 <= torpedo['y'] < MAP_SIZE):
                continue
            if self.in_cave and (not self.cave_is_passable(torpedo['x'], torpedo['y'])):
                continue
            hit = None
            for animal in current_animals:
                if animal.hp <= 0 or getattr(animal, 'is_companion', False):
                    continue
                dx = animal.x - torpedo['x']
                dy = animal.y - torpedo['y']
                visual_radius = max(0.35, animal.type.size / max(1, CELL_SIZE) * 0.32)
                if dx * dx + dy * dy <= visual_radius * visual_radius:
                    hit = animal
                    break
            if hit is None and (not self.in_cave):
                golems = self._near_golems(torpedo['x'], torpedo['y'], 0.9, ('active',))
                if golems:
                    golem = min(golems, key=lambda g: (g.x - torpedo['x']) ** 2 + (g.y - torpedo['y']) ** 2)
                    self._damage_golem(golem, torpedo['damage'])
                    continue
            if hit is None and (not self.in_cave):
                mosses = self._moss_near(torpedo['x'], torpedo['y'], 0.55)
                if mosses:
                    moss = min(mosses, key=lambda m: (m.x - torpedo['x']) ** 2 + (m.y - torpedo['y']) ** 2)
                    self._hit_mad_moss(moss, torpedo['damage'])
                    continue
            if hit is not None:
                hit.hp -= torpedo['damage']
                self.notification = f'Торпеда поразила {hit.type.name}: HP {max(0, int(hit.hp))}'
                self.notification_timer = 45
                if hit.hp <= 0:
                    self.handle_animal_death(hit)
                continue
            survivors.append(torpedo)
        self.vehicle_torpedoes = survivors

    def draw_vehicle_wrecks(self, camera_x, camera_y):
        for wreck in self.vehicle_wrecks:
            if wreck['in_cave'] != self.in_cave:
                continue
            sx = int((wreck['x'] - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = int((wreck['y'] - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            if not (-220 <= sx <= self._render_canvas_size + 220 and -220 <= sy <= self._render_canvas_size + 220):
                continue
            image = self.get_vehicle_rotated(wreck['type'], wreck['angle'])
            if image is not None:
                wreck_image = image.copy()
                wreck_image.set_alpha(115)
                self.screen.blit(wreck_image, wreck_image.get_rect(center=(sx, sy)))
            r = 13 if wreck['type'] is seamoth_type else 18
            pygame.draw.circle(self.screen, (190, 45, 35), (sx, sy), r, 2)
            pygame.draw.line(self.screen, (255, 80, 55), (sx - r, sy - r), (sx + r, sy + r), 3)
            pygame.draw.line(self.screen, (255, 80, 55), (sx - r, sy + r), (sx + r, sy - r), 3)

    def draw_vehicle_torpedoes(self, camera_x, camera_y):
        for torpedo in self.vehicle_torpedoes:
            if torpedo['in_cave'] != self.in_cave:
                continue
            sx = int((torpedo['x'] - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = int((torpedo['y'] - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            dx, dy = (torpedo['vx'], torpedo['vy'])
            length = max(0.001, math.hypot(dx, dy))
            nx, ny = (dx / length, dy / length)
            tail = (int(sx - nx * 10), int(sy - ny * 10))
            pygame.draw.line(self.screen, (85, 210, 235), tail, (sx, sy), 5)
            pygame.draw.circle(self.screen, (240, 245, 210), (sx, sy), 3)

    def _draw_hostile_attacks(self, camera_x, camera_y):
        """Рисует иглы, огненные шары, газовые пузырьки и взрывы."""
        now = pygame.time.get_ticks()
        for projectile in self.hostile_projectiles:
            if projectile['in_cave'] != self.in_cave:
                continue
            sx = int((projectile['x'] - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = int((projectile['y'] - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            kind = projectile['kind']
            if kind == 'needle':
                length = max(7, int(CELL_SIZE * 0.28))
                mag = max(0.001, math.hypot(projectile['vx'], projectile['vy']))
                ux = projectile['vx'] / mag
                uy = projectile['vy'] / mag
                tail = (int(sx - ux * length), int(sy - uy * length))
                pygame.draw.line(self.screen, (55, 75, 35), tail, (sx, sy), 5)
                pygame.draw.line(self.screen, (205, 235, 125), tail, (sx, sy), 2)
                pygame.draw.circle(self.screen, (240, 250, 180), (sx, sy), 2)
            elif kind == 'quest_wave':
                phase = self.frame_count % 24 / 24.0
                r = 12 + int(6 * phase)
                pygame.draw.circle(self.screen, (15, 74, 120), (sx, sy), r + 3, 3)
                pygame.draw.circle(self.screen, (75, 218, 250), (sx, sy), r, 3)
                pygame.draw.circle(self.screen, (250, 164, 44), (sx, sy), 5, 2)
                pygame.draw.circle(self.screen, (240, 255, 255), (sx, sy), 2)
            elif kind == 'fireball':
                pygame.draw.circle(self.screen, (180, 35, 20), (sx, sy), 10)
                pygame.draw.circle(self.screen, (255, 90, 25), (sx, sy), 7)
                pygame.draw.circle(self.screen, (255, 220, 95), (sx, sy), 3)
            else:
                pygame.draw.circle(self.screen, (45, 115, 65), (sx, sy), 7)
                pygame.draw.circle(self.screen, (155, 225, 120), (sx, sy), 5)
                pygame.draw.circle(self.screen, (215, 250, 165), (sx - 1, sy - 2), 2)
        survivors = []
        for effect in self.special_explosions:
            if effect['in_cave'] != self.in_cave:
                survivors.append(effect)
                continue
            if now >= effect['expires_at']:
                continue
            duration = max(1, effect['expires_at'] - effect['created_at'])
            progress = max(0.0, min(1.0, (now - effect['created_at']) / duration))
            sx = int((effect['x'] - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = int((effect['y'] - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            max_radius = max(5, int(effect.get('radius', 1.0) * CELL_SIZE))
            radius = max(3, int(max_radius * (0.35 + 0.65 * progress)))
            if effect['kind'] in ('fireball',):
                pygame.draw.circle(self.screen, (170, 40, 20), (sx, sy), radius)
                pygame.draw.circle(self.screen, (255, 120, 35), (sx, sy), max(2, radius * 2 // 3))
                pygame.draw.circle(self.screen, (255, 230, 120), (sx, sy), max(2, radius // 3))
            else:
                pygame.draw.circle(self.screen, (100, 190, 85), (sx, sy), radius, 3)
                pygame.draw.circle(self.screen, (185, 235, 125), (sx, sy), max(2, radius // 2), 2)
            survivors.append(effect)
        self.special_explosions = survivors

    def get_inventory_image(self, item):
        if item.image is None:
            return None
        key = id(item)
        cached = self.inventory_image_cache.get(key)
        if cached is not None:
            return cached
        image = item.image
        max_size = SLOT_SIZE - 6
        scale = min(max_size / image.get_width(), max_size / image.get_height(), 1.0)
        if scale < 1.0:
            width = max(1, int(image.get_width() * scale))
            height = max(1, int(image.get_height() * scale))
            image = pygame.transform.smoothscale(image, (width, height))
        self.inventory_image_cache[key] = image
        return image

    @staticmethod
    def _move_point_inside_biome(biome, x, y, target_x, target_y):
        """Pull a point towards the biome center until it is inside."""
        for _ in range(30):
            if biome.contains(x, y):
                return (x, y)
            x = (x + target_x) / 2.0
            y = (y + target_y) / 2.0
        return (target_x, target_y)

    def _find_cave_entrance_point(self, biome, angle, inset=18):
        cx, cy = biome.center()
        dx = math.cos(angle)
        dy = math.sin(angle)
        x1, y1, x2, y2 = biome.rect
        max_distance = max(x2 - x1, y2 - y1) * 0.8 + 30
        last_x = cx
        last_y = cy
        distance = 0.0
        while distance <= max_distance:
            px = cx + dx * distance
            py = cy + dy * distance
            if not biome.contains(px, py):
                break
            last_x = px
            last_y = py
            distance += 2.0
        last_x -= dx * inset
        last_y -= dy * inset
        return self._move_point_inside_biome(biome, last_x, last_y, cx, cy)

    def _init_caves(self):
        """Создаёт единую связанную подземную сеть.

        Каждый поверхностный биом получает:
        - один крупный центральный зал;
        - три независимых выхода на поверхность;
        - три боковых зала;
        - локальные коридоры между ними.

        После этого центральные залы всех биомов соединяются минимальным
        остовным деревом и дополнительными перемычками. Поэтому из любой
        пещеры можно под землёй добраться до любой другой, а у каждого
        локального узла существенно больше двух направлений движения.
        """
        self.cave_tunnels = []
        self.cave_entrances = []
        self.cave_object_grid.clear()
        self.cave_flora_grid.clear()
        self.cave_animals = []
        self.cave_hubs = []
        self.cave_connections = []
        self.cave_tunnel_chunks.clear()
        self.cave_tunnel_cache.clear()
        self.surface_cave_entrance_cells.clear()
        self.cave_exit_cells.clear()
        surface_biomes = [biome for biome in self.world.biomes if biome.cave_enabled]
        for biome_index, parent in enumerate(surface_biomes):
            seed = 5000 + biome_index * 7919 + sum(((i + 1) * ord(ch) for i, ch in enumerate(parent.name)))
            rnd = random.Random(seed)
            cx, cy = parent.center()
            x1, y1, x2, y2 = parent.rect
            biome_w = max(20, x2 - x1)
            biome_h = max(20, y2 - y1)
            min_size = min(biome_w, biome_h)
            cave_color = (max(15, parent.color[0] // 3), max(18, parent.color[1] // 3), max(24, parent.color[2] // 3))
            deep_color = (max(10, cave_color[0] - 8), max(12, cave_color[1] - 8), max(18, cave_color[2] - 8))
            hub_rx = max(15, min(30, biome_w * 0.105))
            hub_ry = max(14, min(28, biome_h * 0.105))
            hub_poly = make_blob_points(cx, cy, hub_rx, hub_ry, seed + 99, point_count=18, jitter=0.1)
            cave_resources = [(rock_type, 3)]
            for resource, density in parent.objects:
                if resource in (seaweed_type, sand_type):
                    continue
                cave_resources.append((resource, max(1, density // 2)))
                if len(cave_resources) >= 6:
                    break
            if parent.name in ('Био-люминесцентная зона', 'Горы', 'Центральное море'):
                cave_resources.append((kyanite_type, 1))
            hub = Biome(f'{parent.name}: центральный пещерный узел', deep_color, 0, 0, 0, 0, cave_resources, [], polygon=hub_poly, clip_biome=parent, priority=5)
            hub.parent_surface_biome = parent
            hub.cave_kind = 'hub'
            self.cave_tunnels.append(hub)
            hub_record = {'biome': parent, 'hub': hub, 'point': (cx, cy), 'color': cave_color, 'deep_color': deep_color, 'seed': seed}
            self.cave_hubs.append(hub_record)
            tunnel_width = max(8, min(15, min_size * 0.065))
            entrance_base_angle = rnd.uniform(0, math.tau)
            for entrance_index in range(3):
                entrance_angle = entrance_base_angle + entrance_index * (math.tau / 3.0) + rnd.uniform(-0.16, 0.16)
                entrance_x, entrance_y = self._find_cave_entrance_point(parent, entrance_angle, inset=max(10, int(tunnel_width * 1.7)))
                entrance_x = int(entrance_x)
                entrance_y = int(entrance_y)
                inward_x = cx - entrance_x
                inward_y = cy - entrance_y
                inward_len = math.hypot(inward_x, inward_y)
                if inward_len <= 0.001:
                    inward_x = 1.0
                    inward_y = 0.0
                    inward_len = 1.0
                inward_x /= inward_len
                inward_y /= inward_len
                spawn_x = entrance_x + inward_x * max(5, tunnel_width * 0.8)
                spawn_y = entrance_y + inward_y * max(5, tunnel_width * 0.8)
                spawn_x, spawn_y = self._move_point_inside_biome(parent, spawn_x, spawn_y, cx, cy)
                self.cave_entrances.append({'name': f'{parent.name}: выход {entrance_index + 1}', 'surface_rect': (entrance_x, entrance_y, entrance_x, entrance_y), 'cave_rect': (entrance_x, entrance_y, entrance_x, entrance_y), 'cave_spawn': (spawn_x + 0.5, spawn_y + 0.5), 'surface_spawn': (entrance_x + 0.5, entrance_y + 0.5), 'parent_biome': parent})
                self.world.object_grid.pop((entrance_x, entrance_y), None)
                self.world.flora_grid.pop((entrance_x, entrance_y), None)
                side = -1 if entrance_index % 2 else 1
                perp_x = -inward_y
                perp_y = inward_x
                bend = min_size * rnd.uniform(0.04, 0.1) * side
                mid_x = entrance_x + (cx - entrance_x) * 0.48 + perp_x * bend
                mid_y = entrance_y + (cy - entrance_y) * 0.48 + perp_y * bend
                mid_x, mid_y = self._move_point_inside_biome(parent, mid_x, mid_y, cx, cy)
                for segment_index, (a, b) in enumerate((((entrance_x, entrance_y), (mid_x, mid_y)), ((mid_x, mid_y), (cx, cy)))):
                    corridor_poly = make_corridor_polygon(a[0], a[1], b[0], b[1], tunnel_width)
                    corridor = Biome(f'{parent.name}: выходной тоннель {entrance_index + 1}.{segment_index + 1}', cave_color, 0, 0, 0, 0, [(rock_type, 2)], [], polygon=corridor_poly, clip_biome=parent, priority=2)
                    corridor.parent_surface_biome = parent
                    corridor.cave_kind = 'local_corridor'
                    self.cave_tunnels.append(corridor)
            branch_base_angle = entrance_base_angle + math.pi / 3.0
            branch_points = []
            branch_radius = min_size * 0.24
            for branch_index in range(3):
                branch_angle = branch_base_angle + branch_index * (math.tau / 3.0) + rnd.uniform(-0.18, 0.18)
                bx = cx + math.cos(branch_angle) * branch_radius
                by = cy + math.sin(branch_angle) * branch_radius
                bx, by = self._move_point_inside_biome(parent, bx, by, cx, cy)
                branch_points.append((bx, by))
                room_rx = max(9, min(20, hub_rx * 0.7))
                room_ry = max(8, min(18, hub_ry * 0.7))
                branch_poly = make_blob_points(bx, by, room_rx, room_ry, seed + 300 + branch_index * 17, point_count=14, jitter=0.13)
                branch_room = Biome(f'{parent.name}: боковой зал {branch_index + 1}', deep_color, 0, 0, 0, 0, cave_resources[:4], [], polygon=branch_poly, clip_biome=parent, priority=4)
                branch_room.parent_surface_biome = parent
                branch_room.cave_kind = 'branch_room'
                self.cave_tunnels.append(branch_room)
                branch_corridor = Biome(f'{parent.name}: ответвление {branch_index + 1}', cave_color, 0, 0, 0, 0, [(rock_type, 2)], [], polygon=make_corridor_polygon(cx, cy, bx, by, max(7, tunnel_width * 0.8)), clip_biome=parent, priority=2)
                branch_corridor.parent_surface_biome = parent
                branch_corridor.cave_kind = 'branch_corridor'
                self.cave_tunnels.append(branch_corridor)
            for branch_index in range(3):
                a = branch_points[branch_index]
                b = branch_points[(branch_index + 1) % 3]
                loop_corridor = Biome(f'{parent.name}: кольцевой тоннель {branch_index + 1}', cave_color, 0, 0, 0, 0, [(rock_type, 1)], [], polygon=make_corridor_polygon(a[0], a[1], b[0], b[1], max(6, tunnel_width * 0.62)), clip_biome=parent, priority=1)
                loop_corridor.parent_surface_biome = parent
                loop_corridor.cave_kind = 'local_loop'
                self.cave_tunnels.append(loop_corridor)
        if self.cave_hubs:
            connected = {0}
            global_edges = set()
            while len(connected) < len(self.cave_hubs):
                best = None
                best_distance = None
                for i in connected:
                    ax, ay = self.cave_hubs[i]['point']
                    for j in range(len(self.cave_hubs)):
                        if j in connected:
                            continue
                        bx, by = self.cave_hubs[j]['point']
                        distance = (ax - bx) ** 2 + (ay - by) ** 2
                        if best_distance is None or distance < best_distance:
                            best_distance = distance
                            best = (i, j)
                if best is None:
                    break
                i, j = best
                global_edges.add(tuple(sorted((i, j))))
                connected.add(j)
            for i, hub_record in enumerate(self.cave_hubs):
                ax, ay = hub_record['point']
                candidates = []
                for j, other in enumerate(self.cave_hubs):
                    if i == j:
                        continue
                    bx, by = other['point']
                    distance = (ax - bx) ** 2 + (ay - by) ** 2
                    candidates.append((distance, j))
                candidates.sort()
                for _, j in candidates[:2]:
                    global_edges.add(tuple(sorted((i, j))))
            for edge_index, (i, j) in enumerate(sorted(global_edges)):
                hub_a = self.cave_hubs[i]
                hub_b = self.cave_hubs[j]
                ax, ay = hub_a['point']
                bx, by = hub_b['point']
                link_width = 8
                link_color = (18, 62, 70)
                link = Biome(f"Межбиомный тоннель: {hub_a['biome'].name} ↔ {hub_b['biome'].name}", link_color, 0, 0, 0, 0, [(rock_type, 2), (quartz_type, 1)], [], polygon=make_corridor_polygon(ax, ay, bx, by, link_width), clip_biome=None, priority=1)
                link.cave_kind = 'interbiome_corridor'
                self.cave_tunnels.append(link)
                self.cave_connections.append((i, j))
                distance = math.hypot(bx - ax, by - ay)
                if distance > 170:
                    mx = (ax + bx) / 2.0
                    my = (ay + by) / 2.0
                    junction = Biome(f'Межбиомная развилка {edge_index + 1}', (15, 48, 58), 0, 0, 0, 0, [(rock_type, 2), (quartz_type, 2)], [], polygon=make_blob_points(mx, my, 13, 11, 88000 + edge_index, point_count=14, jitter=0.1), clip_biome=None, priority=3)
                    junction.cave_kind = 'interbiome_junction'
                    self.cave_tunnels.append(junction)
        for tunnel in self.cave_tunnels:
            override = CAVE_TUNNEL_ECOSYSTEM_OVERRIDES.get(tunnel.name, {})
            if 'flora' in override:
                custom_flora = []
                for flora_key, density in override.get('flora', {}).items():
                    flora_type = flora_types.get(flora_key)
                    try:
                        density = float(density)
                    except (TypeError, ValueError):
                        continue
                    if flora_type is not None and density > 0:
                        custom_flora.append((flora_type, density))
                tunnel.flora = custom_flora
                continue
            parent = getattr(tunnel, 'parent_surface_biome', None)
            if parent is None:
                tunnel.flora = []
                continue
            profile = cave_flora_profiles.get(parent.name, ())
            kind = getattr(tunnel, 'cave_kind', '')
            if kind in ('hub', 'branch_room'):
                scale = 1.0
            elif kind in ('local_corridor', 'branch_corridor', 'local_loop'):
                scale = 0.38
            elif kind == 'interbiome_corridor':
                scale = 0.16
            elif kind == 'interbiome_junction':
                scale = 0.3
            else:
                scale = 0.0
            tunnel.flora = [(flora_type, density * scale) for flora_type, density in profile if density * scale > 0]
        self._build_cave_spatial_index()
        self._build_cave_entrance_index()
        cave_exit_cells = set()
        for entrance in self.cave_entrances:
            ex1, ey1, ex2, ey2 = entrance['cave_rect']
            for ey in range(ey1, ey2 + 1):
                for ex in range(ex1, ex2 + 1):
                    cave_exit_cells.add((ex, ey))
        for tunnel_index, tunnel in enumerate(self.cave_tunnels):
            rnd = random.Random(70000 + tunnel_index * 101)
            area = max(1, tunnel.area())
            for object_type, density in tunnel.objects:
                count = int(area / 400 * density)
                if getattr(tunnel, 'cave_kind', '') == 'interbiome_corridor':
                    count = min(count, 12)
                for _ in range(count):
                    for _attempt in range(50):
                        rx, ry = tunnel.random_point(rnd)
                        x = int(rx)
                        y = int(ry)
                        key = (x, y)
                        if key in cave_exit_cells:
                            continue
                        if key in self.cave_object_grid:
                            continue
                        if self.get_cave_tunnel_at(x, y) is not tunnel:
                            continue
                        self.cave_object_grid[key] = object_type
                        break
        for tunnel_index, tunnel in enumerate(self.cave_tunnels):
            if not getattr(tunnel, 'flora', None):
                continue
            area = max(1.0, tunnel.area())
            for flora_index, (flora_type, density) in enumerate(tunnel.flora):
                count = int(area / 400.0 * float(density))
                if density > 0 and count <= 0 and (area >= 80):
                    count = 1
                count = min(count, 420)
                rnd = random.Random(470000 + tunnel_index * 6151 + flora_index * 487)
                placed = 0
                attempts = 0
                max_attempts = max(30, count * 14)
                while placed < count and attempts < max_attempts:
                    attempts += 1
                    rx, ry = tunnel.random_point(rnd)
                    x = int(rx)
                    y = int(ry)
                    key = (x, y)
                    if key in cave_exit_cells:
                        continue
                    if key in self.cave_object_grid:
                        continue
                    if key in self.cave_flora_grid:
                        continue
                    if self.get_cave_tunnel_at(x, y) is not tunnel:
                        continue
                    self.cave_flora_grid[key] = flora_type
                    placed += 1
        cave_fauna_by_parent = {'Мелководье': [(zucchini_fish_type, 5)], 'Глубоководье': [(zucchini_fish_type, 5)], 'Центральное море': [(eggplant_fish_type, 7)], 'Побережье': [], 'Острова': [], 'Коралловый риф': [(eggplant_fish_type, 6)], 'Леса': [], 'Кельповый лес': [(zucchini_fish_type, 4)], 'Био-люминесцентная зона': [(eggplant_fish_type, 6)], 'Горы': [(sea_dragon_type, 1)]}
        cave_rooms_by_parent = {}
        for tunnel in self.cave_tunnels:
            parent = getattr(tunnel, 'parent_surface_biome', None)
            if parent is None:
                continue
            if getattr(tunnel, 'cave_kind', '') not in ('hub', 'branch_room'):
                continue
            cave_rooms_by_parent.setdefault(parent.name, []).append(tunnel)
        for parent_name, species in cave_fauna_by_parent.items():
            rooms = cave_rooms_by_parent.get(parent_name, ())
            if not rooms:
                continue
            seed = 99000 + sum(((i + 1) * ord(ch) for i, ch in enumerate(parent_name)))
            rnd = random.Random(seed)
            for species_index, (animal_type, total_count) in enumerate(species):
                for spawn_index in range(total_count):
                    room = rooms[(spawn_index + species_index) % len(rooms)]
                    spawn = None
                    for _attempt in range(40):
                        x, y = room.random_point(rnd)
                        if self.get_cave_tunnel_at(int(x), int(y)) is room:
                            spawn = (x, y)
                            break
                    if spawn is not None:
                        self.cave_animals.append(Animal(spawn[0], spawn[1], animal_type, room))
        default_fish_by_parent = {'Мелководье': zucchini_fish_type, 'Глубоководье': zucchini_fish_type, 'Центральное море': eggplant_fish_type, 'Побережье': zucchini_fish_type, 'Острова': zucchini_fish_type, 'Коралловый риф': eggplant_fish_type, 'Леса': zucchini_fish_type, 'Кельповый лес': zucchini_fish_type, 'Био-люминесцентная зона': eggplant_fish_type, 'Горы': sea_dragon_type, 'Реки': zucchini_fish_type, 'Озёра (внешние)': zucchini_fish_type}
        for tunnel_index, tunnel in enumerate(self.cave_tunnels):
            override = CAVE_TUNNEL_ECOSYSTEM_OVERRIDES.get(tunnel.name, {})
            if 'fauna' in override:
                continue
            parent = getattr(tunnel, 'parent_surface_biome', None)
            fish_type = default_fish_by_parent.get(getattr(parent, 'name', None))
            if fish_type is None:
                fish_type = (zucchini_fish_type, eggplant_fish_type)[tunnel_index % 2]
            fish_count = 3 if tunnel.area() >= 900 else 2
            rnd = random.Random(126000 + tunnel_index * 733)
            placed = 0
            for _fish_index in range(fish_count):
                for _attempt in range(80):
                    x, y = tunnel.random_point(rnd)
                    ix, iy = (int(x), int(y))
                    if self.get_cave_tunnel_at(ix, iy) is not tunnel:
                        continue
                    self.cave_animals.append(Animal(x, y, fish_type, tunnel))
                    placed += 1
                    break
        cave_animal_registry = {'zucchini_fish': zucchini_fish_type, 'eggplant_fish': eggplant_fish_type, 'sea_dragon': sea_dragon_type, 'chbos_whale': chbos_whale_type, 'cucumber_fish': cucumber_fish_type, 'patisson_fish': patisson_fish_type, 'nut_fish': nut_fish_type, 'bolt_fish': bolt_fish_type}
        for tunnel in self.cave_tunnels:
            override = CAVE_TUNNEL_ECOSYSTEM_OVERRIDES.get(tunnel.name, {})
            if 'fauna' not in override:
                continue
            self.cave_animals = [animal for animal in self.cave_animals if animal.home_biome is not tunnel]
            for animal_key, amount in override.get('fauna', {}).items():
                animal_type = cave_animal_registry.get(animal_key)
                try:
                    amount = max(0, min(100, int(amount)))
                except (TypeError, ValueError):
                    continue
                if animal_type is None or amount <= 0:
                    continue
                rnd = random.Random(191000 + sum(((i + 1) * ord(ch) for i, ch in enumerate(tunnel.name))) + len(animal_key) * 97)
                placed = 0
                for _spawn_index in range(amount):
                    for _attempt in range(80):
                        x, y = tunnel.random_point(rnd)
                        ix, iy = (int(x), int(y))
                        if self.get_cave_tunnel_at(ix, iy) is not tunnel:
                            continue
                        self.cave_animals.append(Animal(x, y, animal_type, tunnel))
                        placed += 1
                        break
        cave_room_counts = {}
        for animal in self.cave_animals:
            room_counts = cave_room_counts.setdefault(animal.home_biome, {})
            room_counts[animal.type] = room_counts.get(animal.type, 0) + 1
        for tunnel in self.cave_tunnels:
            counts = cave_room_counts.get(tunnel, {})
            tunnel.animals = [(animal_type, count) for animal_type, count in counts.items()]

    def _build_cave_spatial_index(self):
        self.cave_tunnel_chunks.clear()
        self.cave_tunnel_cache.clear()
        chunk = CAVE_CHUNK_SIZE
        max_chunk = (MAP_SIZE - 1) // chunk
        for tunnel in self.cave_tunnels:
            x1, y1, x2, y2 = tunnel.rect
            cx1 = max(0, int(x1) // chunk)
            cy1 = max(0, int(y1) // chunk)
            cx2 = min(max_chunk, int(x2) // chunk)
            cy2 = min(max_chunk, int(y2) // chunk)
            for cy in range(cy1, cy2 + 1):
                for cx in range(cx1, cx2 + 1):
                    self.cave_tunnel_chunks.setdefault((cx, cy), []).append(tunnel)

    def _build_cave_entrance_index(self):
        self.surface_cave_entrance_cells.clear()
        self.cave_exit_cells.clear()
        for entrance in self.cave_entrances:
            sx1, sy1, sx2, sy2 = entrance['surface_rect']
            for y in range(sy1, sy2 + 1):
                for x in range(sx1, sx2 + 1):
                    self.surface_cave_entrance_cells[x, y] = entrance
            cx1, cy1, cx2, cy2 = entrance['cave_rect']
            for y in range(cy1, cy2 + 1):
                for x in range(cx1, cx2 + 1):
                    self.cave_exit_cells[x, y] = entrance

    @staticmethod
    def _point_in_rect(x, y, rect):
        x1, y1, x2, y2 = rect
        return x1 <= x <= x2 and y1 <= y <= y2

    def get_cave_tunnel_at(self, x, y):
        if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
            return None
        integer_x = int(x)
        integer_y = int(y)
        cacheable = x == integer_x and y == integer_y
        cache_key = (integer_x, integer_y)
        if cacheable and cache_key in self.cave_tunnel_cache:
            return self.cave_tunnel_cache[cache_key]
        chunk_key = (integer_x // CAVE_CHUNK_SIZE, integer_y // CAVE_CHUNK_SIZE)
        result = None
        for tunnel in reversed(self.cave_tunnel_chunks.get(chunk_key, ())):
            if tunnel.contains(x, y):
                result = tunnel
                break
        if cacheable:
            if len(self.cave_tunnel_cache) >= MAX_CAVE_CACHE:
                self.cave_tunnel_cache.clear()
            self.cave_tunnel_cache[cache_key] = result
        return result

    def cave_is_passable(self, x, y):
        if self.get_cave_tunnel_at(x, y) is None:
            return False
        obj = self.cave_world.get_object_at(int(x), int(y))
        return obj is None or not obj.is_wall

    def get_surface_cave_entrance_at(self, x, y):
        integer_x = int(x)
        integer_y = int(y)
        if x == integer_x and y == integer_y:
            return self.surface_cave_entrance_cells.get((integer_x, integer_y))
        for entrance in self.cave_entrances:
            if self._point_in_rect(x, y, entrance['surface_rect']):
                return entrance
        return None

    def get_cave_exit_at(self, x, y):
        integer_x = int(x)
        integer_y = int(y)
        if x == integer_x and y == integer_y:
            return self.cave_exit_cells.get((integer_x, integer_y))
        for entrance in self.cave_entrances:
            if self._point_in_rect(x, y, entrance['cave_rect']):
                return entrance
        return None

    def get_current_animals(self):
        if self.in_cave:
            return self.cave_animals
        return self.world.animals

    def get_nearby_animals(self, radius):
        if self.in_cave:
            player_x = self.player_x
            player_y = self.player_y
            return [animal for animal in self.cave_animals if abs(animal.x - player_x) <= radius and abs(animal.y - player_y) <= radius]
        return self.world.get_animals_near(self.player_x, self.player_y, radius)

    def remove_current_animal(self, animal):
        self._whale_quest_on_whale_death(animal)
        if self.in_cave:
            if animal in self.cave_animals:
                self.cave_animals.remove(animal)
            return
        self.world.remove_animal(animal)

    def _get_all_companions(self):
        result = []
        seen = set()
        for animal in list(self.world.animals) + list(self.cave_animals):
            if not getattr(animal, 'is_companion', False):
                continue
            marker = id(animal)
            if marker in seen:
                continue
            seen.add(marker)
            result.append(animal)
        return result

    def _get_visible_companions(self):
        companions = []
        for animal in self.get_current_animals():
            if not getattr(animal, 'is_companion', False):
                continue
            if abs(animal.x - self.player_x) <= VIEW_RADIUS and abs(animal.y - self.player_y) <= VIEW_RADIUS:
                companions.append(animal)
        return companions

    def _companion_follow_distance(self, animal):
        length = float(getattr(animal.type, 'fish_length', max(20, animal.type.size * 3)))
        return max(1.7, min(VIEW_RADIUS * 0.68, 1.35 + length / 55.0))

    def _companion_orbit_distance(self, animal):
        follow = self._companion_follow_distance(animal)
        return max(1.5, min(VIEW_RADIUS * 0.72, follow * 0.92))

    def _position_is_passable_for_companion(self, x, y):
        if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
            return False
        if self.in_cave:
            return self.cave_is_passable(x, y)
        cell_x = int(x)
        cell_y = int(y)
        target_object = self.world.get_object_at(cell_x, cell_y)
        if target_object is not None and target_object.is_wall:
            return False
        if self.is_start_capsule_wall(cell_x, cell_y):
            return False
        biome = self.world.get_biome_at(cell_x, cell_y)
        if is_land_biome(biome):
            return False
        if self.is_start_capsule_passage(cell_x, cell_y):
            return True
        return biome is not None and biome.passable

    def _find_nearby_passable_position(self, radius=1.25):
        candidates = [(self.player_x, self.player_y)]
        for ring in (radius, radius * 1.6):
            for index in range(12):
                angle = index / 12.0 * math.tau
                candidates.append((self.player_x + math.cos(angle) * ring, self.player_y + math.sin(angle) * ring))
        for x, y in candidates:
            if self._position_is_passable_for_companion(x, y):
                return (x, y)
        return None

    def _add_animal_to_current_layer(self, animal):
        if self.in_cave:
            self.cave_animals.append(animal)
        else:
            self.world.animals.append(animal)
            self.world.rebuild_animal_index()

    def _clear_companion_target(self, animal):
        target = getattr(animal, 'companion_target', None)
        if target is not None and getattr(target, 'pet_combat_target', None) is animal:
            target.pet_combat_target = None
        animal.companion_target = None

    def _transfer_visible_companions(self, companions, target_in_cave):
        if not companions:
            return
        if not target_in_cave and self._is_player_on_land():
            return
        for index, animal in enumerate(companions):
            self._clear_companion_target(animal)
            if target_in_cave:
                self.world.remove_animal(animal)
                if animal not in self.cave_animals:
                    self.cave_animals.append(animal)
                animal.home_biome = self.get_cave_tunnel_at(self.player_x, self.player_y) or animal.home_biome
            else:
                if animal in self.cave_animals:
                    self.cave_animals.remove(animal)
                if animal not in self.world.animals:
                    self.world.animals.append(animal)
                animal.home_biome = self.world.get_biome_at(self.player_x, self.player_y) or animal.home_biome
            angle = animal.companion_orbit_angle + index * 0.8
            radius = min(self._companion_follow_distance(animal), VIEW_RADIUS * 0.6)
            target_x = self.player_x + math.cos(angle) * radius
            target_y = self.player_y + math.sin(angle) * radius
            if self._position_is_passable_for_companion(target_x, target_y):
                animal.x = target_x
                animal.y = target_y
            else:
                animal.x = self.player_x
                animal.y = self.player_y
            animal.dx = 0.0
            animal.dy = 0.0
            animal.active = True
            animal.companion_state = 'следует за игроком'
        if not target_in_cave:
            self.world.rebuild_animal_index()

    def _find_companion_hunt_target(self, companion):
        special_combatants = {'Морской дракон-левиафан'}
        if companion.type.behavior != 'predator' and companion.type.name not in special_combatants:
            return None
        best = None
        best_distance_sq = float('inf')
        for animal in self.get_nearby_animals(VIEW_RADIUS):
            if animal is companion or animal.hp <= 0:
                continue
            if getattr(animal, 'is_companion', False):
                continue
            if abs(animal.x - self.player_x) > VIEW_RADIUS or abs(animal.y - self.player_y) > VIEW_RADIUS:
                continue
            dx = animal.x - companion.x
            dy = animal.y - companion.y
            distance_sq = dx * dx + dy * dy
            if distance_sq > VIEW_RADIUS * VIEW_RADIUS:
                continue
            if distance_sq < best_distance_sq:
                best = animal
                best_distance_sq = distance_sq
        return best

    def _move_companion_towards(self, animal, target_x, target_y, move_speed):
        to_target_x = target_x - animal.x
        to_target_y = target_y - animal.y
        distance = math.hypot(to_target_x, to_target_y)
        if distance <= 0.001:
            animal.dx = 0.0
            animal.dy = 0.0
            return False
        step = min(move_speed, distance)
        velocity_x = to_target_x / distance * step
        velocity_y = to_target_y / distance * step
        new_x = animal.x + velocity_x
        new_y = animal.y + velocity_y
        moved = False
        if self._position_is_passable_for_companion(new_x, new_y):
            animal.x = new_x
            animal.y = new_y
            moved = True
        elif self._position_is_passable_for_companion(new_x, animal.y):
            animal.x = new_x
            velocity_y = 0.0
            moved = True
        elif self._position_is_passable_for_companion(animal.x, new_y):
            animal.y = new_y
            velocity_x = 0.0
            moved = True
        else:
            animal.companion_orbit_angle += 0.35
        if moved:
            animal.dx = velocity_x
            animal.dy = velocity_y
            animal._update_rotation()
        else:
            animal.dx = 0.0
            animal.dy = 0.0
        return moved

    def _finish_companion_kill(self, companion, target):
        target.hp = 0
        self._whale_quest_on_whale_death(target)
        if getattr(target, 'pet_combat_target', None) is companion:
            target.pet_combat_target = None
        companion.companion_target = None
        if self.add_to_inventory(target.type, wild_catch=True):
            self.remove_current_animal(target)
            companion.companion_state = 'добыча отправлена в инвентарь'
            self.notification = f'{companion.type.name} поймал {target.type.name}: добыча в инвентаре'
        else:
            companion.companion_state = 'инвентарь полон'
            self.notification = f'{companion.type.name} убил {target.type.name}, но инвентарь полон'
        self.notification_timer = 75

    def _update_companion(self, animal, player_move_x, player_move_y):
        dx_to_player = self.player_x - animal.x
        dy_to_player = self.player_y - animal.y
        distance_to_player = math.hypot(dx_to_player, dy_to_player)
        if animal.companion_attack_timer > 0:
            animal.companion_attack_timer -= 1
        if abs(dx_to_player) > VIEW_RADIUS or abs(dy_to_player) > VIEW_RADIUS:
            self._clear_companion_target(animal)
            animal.active = False
            animal.dx = 0.0
            animal.dy = 0.0
            animal.companion_state = 'ждёт вне видимости'
            return
        animal.active = True
        player_move_length = math.hypot(player_move_x, player_move_y)
        follow_distance = self._companion_follow_distance(animal)
        orbit_distance = self._companion_orbit_distance(animal)
        animal.companion_orbit_radius = orbit_distance
        must_follow = player_move_length > 0.001 or distance_to_player > follow_distance * 1.35
        if must_follow:
            self._clear_companion_target(animal)
            if player_move_length > 0.001:
                move_x = player_move_x / player_move_length
                move_y = player_move_y / player_move_length
            elif distance_to_player > 0.001:
                move_x = -dx_to_player / distance_to_player
                move_y = -dy_to_player / distance_to_player
            else:
                move_x, move_y = (1.0, 0.0)
            side_x = -move_y
            side_y = move_x
            animal.companion_orbit_angle = (animal.companion_orbit_angle + 0.012) % math.tau
            lateral = math.sin(animal.companion_orbit_angle) * min(1.2, follow_distance * 0.22)
            target_x = self.player_x - move_x * follow_distance + side_x * lateral
            target_y = self.player_y - move_y * follow_distance + side_y * lateral
            speed_scale = 0.75 + min(0.55, follow_distance / 10.0)
            move_speed = max(0.22, min(1.15, animal.type.speed * 8.0 * speed_scale))
            if distance_to_player > VIEW_RADIUS * 0.65:
                move_speed *= 1.35
            self._move_companion_towards(animal, target_x, target_y, move_speed)
            animal.companion_state = 'следует за игроком'
            return
        target = getattr(animal, 'companion_target', None)
        current_animals = self.get_current_animals()
        target_valid = target is not None and target in current_animals and (target.hp > 0) and (not getattr(target, 'is_companion', False)) and (abs(target.x - self.player_x) <= VIEW_RADIUS) and (abs(target.y - self.player_y) <= VIEW_RADIUS)
        if not target_valid:
            self._clear_companion_target(animal)
            target = self._find_companion_hunt_target(animal)
            animal.companion_target = target
        if target is not None:
            if target.type.behavior == 'predator':
                target.pet_combat_target = animal
            hunt_dx = target.x - animal.x
            hunt_dy = target.y - animal.y
            hunt_distance = math.hypot(hunt_dx, hunt_dy)
            animal.companion_state = f'охотится: {target.type.name}'
            length = float(getattr(animal.type, 'fish_length', animal.type.size * 3))
            uses_projectiles = animal.type.name in ('Морской дракон-левиафан')
            attack_range = 3.5 if uses_projectiles else max(0.9, min(2.2, 0.75 + length / 150.0))
            if hunt_distance > attack_range:
                hunt_speed = max(0.28, min(1.25, animal.type.speed * 9.5))
                self._move_companion_towards(animal, target.x, target.y, hunt_speed)
            else:
                animal.dx = 0.0
                animal.dy = 0.0
                if animal.companion_attack_timer <= 0 and (not uses_projectiles):
                    damage = max(1, int(animal.type.damage))
                    target.hp -= damage
                    animal.companion_attack_timer = animal.companion_attack_cooldown
                    animal.companion_state = f'атакует: {target.type.name}'
                    if target.hp <= 0:
                        self._finish_companion_kill(animal, target)
                elif uses_projectiles:
                    animal.companion_state = f'готовит особую атаку: {target.type.name}'
            return
        animal.companion_orbit_angle = (animal.companion_orbit_angle + 0.028) % math.tau
        target_x = self.player_x + math.cos(animal.companion_orbit_angle) * orbit_distance
        target_y = self.player_y + math.sin(animal.companion_orbit_angle) * orbit_distance
        orbit_speed = max(0.16, min(0.42, animal.type.speed * 3.4))
        self._move_companion_towards(animal, target_x, target_y, orbit_speed)
        animal.companion_state = 'плавает вокруг игрока'

    def handle_animal_death(self, animal):
        self._whale_quest_on_whale_death(animal)
        if self.add_to_inventory(animal.type, wild_catch=not getattr(animal, 'is_companion', False)):
            self.remove_current_animal(animal)
            self.notification = f'{animal.type.name} убит и подобран!'
        else:
            self.notification = f'{animal.type.name} убит, но инвентарь полон!'
        self.notification_timer = 60

    def use_cave_entrance(self):
        if self.state != 'playing':
            return
        px = int(self.player_x)
        py = int(self.player_y)
        if not self.in_cave:
            entrance = self.get_surface_cave_entrance_at(px, py)
            if entrance is None:
                self.notification = 'Здесь нет входа в пещеру'
                self.notification_timer = 45
                return
            visible_companions = self._get_visible_companions()
            self.in_cave = True
            self.player_x, self.player_y = entrance['cave_spawn']
            self._transfer_visible_companions(visible_companions, target_in_cave=True)
            self.map_center_x = self.player_x
            self.map_center_y = self.player_y
            self.selected_biome = None
            self.notification = f"Вы вошли в пещеру: {entrance['name']}"
            self.notification_timer = 90
            return
        exit_data = self.get_cave_exit_at(px, py)
        if exit_data is None:
            self.notification = 'Всплыть можно только у выхода на поверхность'
            self.notification_timer = 60
            return
        visible_companions = self._get_visible_companions()
        self.in_cave = False
        self.player_x, self.player_y = exit_data['surface_spawn']
        self._transfer_visible_companions(visible_companions, target_in_cave=False)
        self.map_center_x = self.player_x
        self.map_center_y = self.player_y
        self.selected_biome = None
        self.notification = f"Вы всплыли: {exit_data['name']}"
        self.notification_timer = 90

    def _start_inventory_registry(self):
        """Возвращает словарь доступных типов предметов по имени.

        Поддерживаются:
        1) отображаемое имя из игры, например "Мотылёк" или "Нож";
        2) внутреннее имя переменной, если оно было доступно при init_assets().

        START_ITEM_REGISTRY формируется в конце init_assets(), поэтому сюда
        попадают и глобальные, и локальные игровые типы.
        """
        registry = dict(START_ITEM_REGISTRY)
        for variable_name, value in globals().items():
            if isinstance(value, (ObjectType, AnimalType)):
                registry.setdefault(value.name, value)
                registry.setdefault(variable_name, value)
        return registry

    def _load_start_inventory(self):
        """Заполняет инвентарь согласно START_INVENTORY из начала файла."""
        if not isinstance(START_INVENTORY, dict):
            print("[START_INVENTORY] Ошибка: START_INVENTORY должен быть словарём, например {'Сканер': 1, 'Титан': 5}.")
            return
        registry = self._start_inventory_registry()
        added_total = 0
        for item_name, amount in START_INVENTORY.items():
            try:
                amount = int(amount)
            except (TypeError, ValueError):
                print(f"[START_INVENTORY] Пропущено '{item_name}': количество должно быть целым числом, получено {amount!r}.")
                continue
            if amount <= 0:
                continue
            item_type = registry.get(item_name)
            if item_type is None:
                print(f'[START_INVENTORY] Не найден предмет: {item_name!r}. Проверьте точное игровое название.')
                continue
            for _ in range(amount):
                if not self.add_to_inventory(item_type):
                    print(f'[START_INVENTORY] Инвентарь заполнен. Не удалось добавить оставшиеся предметы, начиная с {item_type.name!r}.')
                    return
                added_total += 1
        if added_total:
            print(f'[START_INVENTORY] Добавлено стартовых предметов: {added_total}/{INVENTORY_SIZE}.')

    def add_to_inventory(self, item_type, wild_catch=False):
        for index, slot in enumerate(self.inventory):
            if slot is None:
                self._stored_companion_hp.pop(index, None)
                self.inventory[index] = item_type
                self.inventory_counts[item_type] = self.inventory_counts.get(item_type, 0) + 1
                if wild_catch and hasattr(self, 'whale_quests'):
                    self._whale_quest_on_catch(item_type, index)
                return True
        return False

    def _remove_inventory_slot(self, index):
        if not 0 <= index < INVENTORY_SIZE:
            return None
        item = self.inventory[index]
        if item is None:
            return None
        self.inventory[index] = None
        pass
        self._stored_companion_hp.pop(index, None)
        current_count = self.inventory_counts.get(item, 0) - 1
        if current_count > 0:
            self.inventory_counts[item] = current_count
        else:
            self.inventory_counts.pop(item, None)
            if self.equipped_weapon is item:
                self.equipped_weapon = None
        self.active_slot_index = -1
        return item

    def _current_dropped_items(self):
        return self.cave_dropped_items if self.in_cave else self.surface_dropped_items

    def _spawn_animal_from_inventory_item(self, animal_type, companion=False):
        spawn = self._find_nearby_passable_position(1.25)
        if spawn is None:
            return None
        if self.in_cave:
            home_biome = self.get_cave_tunnel_at(spawn[0], spawn[1])
        else:
            home_biome = self.world.get_biome_at(spawn[0], spawn[1])
            if is_land_biome(home_biome):
                return None
        if home_biome is None and (not companion):
            return None
        if home_biome is None:
            home_biome = self.get_cave_tunnel_at(self.player_x, self.player_y) if self.in_cave else self.world.get_biome_at(self.player_x, self.player_y)
        if home_biome is None:
            return None
        animal = Animal(spawn[0], spawn[1], animal_type, home_biome)
        if companion:
            animal.is_companion = True
            animal.hp = animal.max_hp
            animal.companion_orbit_angle = random.uniform(0.0, math.tau)
            animal.companion_orbit_radius = self._companion_orbit_distance(animal)
            animal.companion_target = None
            animal.companion_attack_timer = 0
            animal.companion_state = 'следует за игроком'
        self._add_animal_to_current_layer(animal)
        return animal

    def drop_selected_item(self):
        if self.state != 'inventory':
            return
        if self.active_slot_index == -1:
            self.notification = 'Выберите предмет в инвентаре'
            self.notification_timer = 60
            return
        item = self.inventory[self.active_slot_index]
        if item is None:
            self.notification = 'Слот пуст'
            self.notification_timer = 45
            return
        if isinstance(item, AnimalType):
            spawned = self._spawn_animal_from_inventory_item(item, companion=False)
            if spawned is None:
                self.notification = 'Здесь нельзя выпустить это существо'
                self.notification_timer = 60
                return
            self._remove_inventory_slot(self.active_slot_index)
            self._whale_quest_on_drop()
            self.notification = f'Выпущено: {item.name}'
            self.notification_timer = 60
            return
        if item is mad_moss_item:
            if self.in_cave:
                self.notification = 'Бешеный мох можно выпускать только на поверхности суши'
                self.notification_timer = 90
                return
            if len(self.world.moss_colonies) >= MAD_MOSS_MAX_COLONIES:
                self.notification = 'Слишком много живых колоний мха'
                self.notification_timer = 90
                return
            moss_spot = None
            for distance in (0.4, 0.65, 0.15, 1.0):
                for index in range(12):
                    angle = index / 12.0 * math.tau + random.uniform(-0.12, 0.12)
                    x = self.player_x + math.cos(angle) * distance
                    y = self.player_y + math.sin(angle) * distance
                    biome = self.world.get_biome_at(x, y)
                    if is_land_biome(biome) and biome.passable and (self.world.get_object_at(x, y) is None):
                        moss_spot = (x, y, biome)
                        break
                if moss_spot is not None:
                    break
            if moss_spot is None:
                self.notification = 'Для живого мха нужна свободная суша поблизости'
                self.notification_timer = 90
                return
            x, y, biome = moss_spot
            self.world.moss_colonies.append(MadMossColony(x, y, biome, diameter=0.3, terminal=False))
            self._remove_inventory_slot(self.active_slot_index)
            self._whale_quest_on_drop()
            self.notification = 'Бешеный мох выпущен: колония ожила и снова растёт'
            self.notification_timer = 110
            return
        cell = (int(self.player_x), int(self.player_y))
        dropped = self._current_dropped_items()
        dropped.setdefault(cell, []).append(item)
        self._remove_inventory_slot(self.active_slot_index)
        self._whale_quest_on_drop()
        self.notification = f'Выброшено: {item.name}'
        self.notification_timer = 60

    def spawn_selected_companion(self):
        if self.state != 'inventory':
            return
        if self.active_slot_index == -1:
            self.notification = 'Выберите рыбу или хищника'
            self.notification_timer = 60
            return
        item = self.inventory[self.active_slot_index]
        if item is None:
            self.notification = 'Слот пуст'
            self.notification_timer = 45
            return
        if not isinstance(item, AnimalType):
            self.notification = 'Спутником может стать только существо'
            self.notification_timer = 60
            return
        slot_index = self.active_slot_index
        companion = self._spawn_animal_from_inventory_item(item, companion=True)
        if companion is None:
            self.notification = 'Здесь недостаточно места для существа'
            self.notification_timer = 60
            return
        stored = self._stored_companion_hp.get(slot_index)
        if stored is not None and stored[0] is item:
            companion.hp = max(1, min(companion.max_hp, stored[1]))
        pass
        self._remove_inventory_slot(slot_index)
        self.notification = f'{item.name} теперь ваш спутник'
        self.notification_timer = 90

    def player_take_damage(self, damage, source=None):
        damage_multiplier = 1.0
        if self.inventory_counts.get(reinforced_suit, 0) > 0:
            damage_multiplier *= 0.75
        if self.damage_resist_timer > 0:
            damage_multiplier *= 0.5
        final_damage = max(1, int(round(damage * damage_multiplier)))
        self.player_hp -= final_damage
        pass
        self.damage_vignette_flash = max(self.damage_vignette_flash, 18)
        self.notification = f'Получено {final_damage} урона! HP: {self.player_hp}'
        self.notification_timer = 60
        if self.player_hp <= 0:
            self.player_hp = 0
            self.notification = 'Игрок погиб! Игра окончена.'
            self.notification_timer = 9999
            self.running = False

    def equip_weapon(self):
        if self.state != 'playing':
            return
        if self.active_slot_index == -1:
            self.notification = 'Выберите предмет в инвентаре'
            self.notification_timer = 60
            return
        item = self.inventory[self.active_slot_index]
        if item is None:
            self.notification = 'Слот пуст'
            self.notification_timer = 60
            return
        if not item.is_weapon:
            self.notification = 'Этот предмет не является оружием'
            self.notification_timer = 60
            return
        self.equipped_weapon = item
        self.notification = f'Экипировано: {item.name}'
        self.notification_timer = 60

    def _consume_selected_item(self, item=None):
        if self.active_slot_index == -1:
            return False
        if item is None:
            item = self.inventory[self.active_slot_index]
        if item is None:
            return False
        self.inventory[self.active_slot_index] = None
        pass
        self._stored_companion_hp.pop(self.active_slot_index, None)
        current_count = self.inventory_counts.get(item, 0) - 1
        if current_count > 0:
            self.inventory_counts[item] = current_count
        else:
            self.inventory_counts.pop(item, None)
            if self.equipped_weapon is item:
                self.equipped_weapon = None
        self.active_slot_index = -1
        return True

    def _get_current_object_grid(self):
        return self.cave_object_grid if self.in_cave else self.world.object_grid

    def _nearby_buildables(self, radius=3.0):
        grid = self._get_current_object_grid()
        result = []
        radius_sq = radius * radius
        for (x, y), obj in grid.items():
            if not getattr(obj, 'is_buildable', False):
                continue
            dx = x - self.player_x
            dy = y - self.player_y
            dist_sq = dx * dx + dy * dy
            if dist_sq <= radius_sq:
                result.append((obj, x, y, dist_sq))
        return result

    def _nearest_resource_info(self, radius=8):
        grid = self._get_current_object_grid()
        radius_sq = radius * radius
        best = None
        highlights = []
        counts = {}
        for (x, y), obj in grid.items():
            if getattr(obj, 'is_buildable', False) or getattr(obj, 'is_weapon', False):
                continue
            dx = x - self.player_x
            dy = y - self.player_y
            dist_sq = dx * dx + dy * dy
            if dist_sq > radius_sq:
                continue
            highlights.append((x, y))
            counts[obj.name] = counts.get(obj.name, 0) + 1
            if best is None or dist_sq < best[0]:
                best = (dist_sq, obj, x, y)
        return (best, counts, highlights)

    def _direction_hint(self, target_x, target_y):
        dx = target_x - self.player_x
        dy = target_y - self.player_y
        parts = []
        if abs(dx) >= 0.5:
            parts.append('восток' if dx > 0 else 'запад')
        if abs(dy) >= 0.5:
            parts.append('юг' if dy > 0 else 'север')
        return '-'.join(parts) if parts else 'здесь'

    def _find_nearest_path_target(self):
        points = []
        if self.in_cave:
            for (x, y), exit_data in self.cave_exit_cells.items():
                points.append((x, y, 'выход на поверхность'))
        else:
            for (x, y), entrance in self.surface_cave_entrance_cells.items():
                points.append((x, y, 'вход в пещеру'))
            x1, y1, x2, y2 = self.start_capsule_rect
            points.append(((x1 + x2) / 2, (y1 + y2) / 2, 'стартовая капсула'))
            for (x, y), obj in self.world.object_grid.items():
                if obj is beacon_type:
                    points.append((x, y, 'радиомаяк'))
        if not points:
            return None
        best = None
        for x, y, label in points:
            dx = x - self.player_x
            dy = y - self.player_y
            dist_sq = dx * dx + dy * dy
            if best is None or dist_sq < best[0]:
                best = (dist_sq, x, y, label)
        return best

    def _scan_environment(self, passive=False):
        radius = 8
        if self.inventory_counts.get(scanner_range, 0) > 0:
            radius += 3
        best, counts, highlights = self._nearest_resource_info(radius)
        self.scanner_highlight_cells = set(highlights[:24])
        self.scanner_highlight_timer = 300
        predator_count = 0
        for animal in self.get_current_animals():
            if animal.hp <= 0:
                continue
            dx = animal.x - self.player_x
            dy = animal.y - self.player_y
            if dx * dx + dy * dy <= radius * radius and animal.type.behavior == 'predator':
                predator_count += 1
        if best is None:
            if not passive:
                self.notification = f'Сканер: поблизости ничего не найдено. Хищников: {predator_count}'
                self.notification_timer = 120
            return
        _, obj, x, y = best
        direction = self._direction_hint(x, y)
        details = ', '.join((f'{name}×{count}' for name, count in list(sorted(counts.items()))[:3]))
        if passive:
            self.notification = f'Комната сканирования: {obj.name} ({direction}), хищников: {predator_count}'
            self.notification_timer = 90
        else:
            self.notification = f'Сканер: ближайший ресурс — {obj.name} ({direction}). Рядом: {details}. Хищников: {predator_count}'
            self.notification_timer = 150

    def _selected_inventory_item(self):
        if not 0 <= self.active_slot_index < len(self.inventory):
            return None
        return self.inventory[self.active_slot_index]

    def _is_scanner_selected(self):
        return self._selected_inventory_item() is scanner_type

    def _current_flora_grid(self):
        return self.cave_flora_grid if self.in_cave else self.world.flora_grid

    def _current_location_name(self, x, y):
        if self.in_cave:
            area = self.get_cave_tunnel_at(x, y)
        else:
            area = self.world.get_biome_at(x, y)
        return area.name if area is not None else 'Неизвестная область'

    def _scan_target_at_cursor(self):
        """Возвращает объект под прицелом сканера.

        Приоритет отдаётся существам, потому что их спрайт может занимать
        несколько клеток. Затем проверяются предметы/ресурсы, растения и
        выброшенные предметы в клетке курсора.
        """
        mx = float(self.mouse_world_x)
        my = float(self.mouse_world_y)
        if not self.in_cave:
            matching = []
            for g in self.world.golem_colonies:
                if g.phase == 'spore':
                    scan_radius = 0.5
                else:
                    scan_radius = max(0.6, GOLEM_DATA[g.kind]['diameter'] * 0.43)
                dist_sq = (g.x + 0.5 - mx) ** 2 + (g.y + 0.5 - my) ** 2
                if dist_sq <= scan_radius ** 2:
                    matching.append((dist_sq, g))
            if matching:
                _, g = min(matching, key=lambda item: item[0])
                bacteria = g.phase == 'spore'
                return {'kind': 'bacteria' if bacteria else 'golem', 'entity': GOLEM_BACTERIA_SCAN_TYPES[g.kind] if bacteria else GOLEM_SCAN_TYPES[g.kind], 'colony': g, 'x': g.x, 'y': g.y}
        best_animal = None
        for animal in self.get_current_animals():
            if animal.hp <= 0:
                continue
            radius = max(0.55, min(3.8, animal.type.size / CELL_SIZE * 0.55))
            dist_sq = (animal.x + 0.5 - mx) ** 2 + (animal.y + 0.5 - my) ** 2
            if dist_sq <= radius * radius:
                if best_animal is None or dist_sq < best_animal[0]:
                    best_animal = (dist_sq, animal)
        if best_animal is not None:
            animal = best_animal[1]
            return {'kind': 'animal', 'entity': animal.type, 'animal': animal, 'x': animal.x, 'y': animal.y}
        x = int(math.floor(mx))
        y = int(math.floor(my))
        if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
            return None
        obj = self._get_current_object_grid().get((x, y))
        if obj is not None:
            return {'kind': 'object', 'entity': obj, 'x': x, 'y': y}
        flora = self._current_flora_grid().get((x, y))
        if flora is not None:
            return {'kind': 'flora', 'entity': flora, 'x': x, 'y': y}
        if not self.in_cave:
            nearest_moss = min((m for m in self.world.moss_colonies if (m.x + 0.5 - mx) ** 2 + (m.y + 0.5 - my) ** 2 <= max(0.2, m.diameter * 0.54) ** 2), key=lambda m: (m.x + 0.5 - mx) ** 2 + (m.y + 0.5 - my) ** 2, default=None)
            if nearest_moss is not None:
                return {'kind': 'moss', 'entity': mad_moss_item, 'colony': nearest_moss, 'x': nearest_moss.x, 'y': nearest_moss.y}
            closest_tree = None
            for tx in range(x - 1, x + 2):
                for ty in range(y - 1, y + 2):
                    tree = self.world.tree_grid.get((tx, ty))
                    if tree is None:
                        continue
                    distance = (tx + 0.5 - mx) ** 2 + (ty + 0.5 - my) ** 2
                    if distance <= 1.2 and (closest_tree is None or distance < closest_tree[0]):
                        closest_tree = (distance, tx, ty, tree)
            if closest_tree is not None:
                _, tx, ty, tree = closest_tree
                return {'kind': 'tree', 'entity': _land_tree_scanner_type(tree[0]), 'x': tx, 'y': ty}
        dropped = self._current_dropped_items().get((x, y))
        if dropped:
            return {'kind': 'dropped', 'entity': dropped[-1], 'x': x, 'y': y}
        return None

    @staticmethod
    def _behavior_name(behavior):
        return {'predator': 'хищник', 'prey': 'мирное / добыча', 'neutral': 'нейтральное'}.get(behavior, str(behavior))

    def _scanner_details(self, target):
        entity = target['entity']
        if isinstance(entity, AnimalType) and entity.name == WHALE_NAME:
            return ['Главный «жаворонок»: просыпается строго в 04:20.', 'Длина: как у дракона-левиафана. HP: 10000. Скорость: 2 клетки/сек.', 'Волна не наносит урона, но навязывает безумный квест. Отказ невозможен!', 'Пример 1: не есть 20 игровых часов.', 'Пример 2: получить удар сталкера за 2 часа.', 'Пример 3: проплыть 200 клеток за 3 часа.', 'Новые задания: выбросить весь инвентарь или уничтожить кита за 4 часа.', 'Или поймать и приручить 50 пискунов за час. Да, пятьдесят.', 'А ещё: собрать все встречающиеся виды фауны за 2 дня.', 'Несколько квестов могут действовать одновременно. Список — в инвентаре.', 'Провал каждого: 20 драконов-левиафанов в воде.', 'Пищевая ценность: +90 еды, +90 воды, +24 HP.', 'На вкус относительно неплох. Но он вряд ли разрешит попробовать себя, если только это не очередной его квест.']
        if target['kind'] in ('golem', 'bacteria'):
            g = target['colony']
            data = GOLEM_DATA[g.kind]
            form = {'spore': 'свободные невидимые бактерии, ветер', 'breeding': 'массовое деление внутри материала', 'active': 'единый живой голем', 'rubble': 'разбитое тело, бактерии ещё живы'}[g.phase]
            return ['КЛАССИФИКАЦИЯ: Големогенные бактерии. Размер особи: 1 мкм.', 'Статус: самый разумный вид на планете. Не спорьте: вас уже просчитали.', 'Материал-носитель: ' + {'stone': 'камень', 'water': 'вода / водоём / дождь', 'wood': 'древесина', 'dung': 'говно / навоз'}[g.kind] + '.', 'Текущее состояние: ' + form + '.', 'Физическим уроном можно развалить голема, но НЕЛЬЗЯ убить бактерии.', 'Для полной ликвидации требуется укол антибиотика (выбрать шприц и U).', 'Ветер разносит микроскопические особи, пока те не попадут на любимый материал.', 'Там происходит взрывное деление, а достигшая критической массы колония', 'создаёт ходячее тело без мозга, мышц, нервов, диплома и разрешения на строительство.', 'Каждая бактерия отдельно вряд ли помнит таблицу умножения.', 'Но в составе голема миллиард коллег коллективно умнее всех живых существ.', 'Высота / размер: около %.1f м; HP: %d; урон: %d.' % (data['diameter'], data['hp'], data['damage']), 'Состояние HP: %d / %d.' % (g.hp, g.max_hp), 'Положение: ' + self._current_location_name(g.x, g.y) + '.', 'Каменный голем — самый крепкий, но даже он не сильнее 50 HP.', 'Водяной быстрее в воде и сильно портит жажду.', 'Деревянный размахивает ветвями; говняный деморализует запахом.', 'Разрушенная оболочка может снова ожить: бактерии собирают новую.', 'Совет КПК: не дразните их. Именно они написали этот совет.', 'Примечание лаборатории: интеллект проверен. Испытуемые победили', 'учёных в шахматы, не имея рук. Шахматную доску тоже сделали сами.']
        location = self._current_location_name(target['x'], target['y'])
        lines = []
        if target['kind'] == 'moss':
            colony = target['colony']
            lines.extend(['Класс: наземная подвижная флора', 'Среда обитания: леса', 'Скорость: 0.1 клетки за кадр', f'Диаметр колонии: {colony.diameter:.2f} клетки', 'Растёт во все стороны и меняет направление случайно.', 'При ударе большая колония распадается на две маленькие.', 'Собирать можно только фрагменты диаметром 0.2 клетки.', 'При употреблении: +1 еда, +4 вода.', f'Обнаружено: {location}'])
        elif target['kind'] == 'tree':
            lines.extend(['Класс: наземная флора / дерево', f'Вид: {entity.name}', 'Среда обитания: леса', 'Крона формируется из листвы или хвои.', f'Обнаружено: {location}'])
        elif target['kind'] == 'flora':
            lines.extend(['Класс: флора', f'Вид: {entity.kind}', f'Размер природного экземпляра: {entity.size}px', f'Обнаружено: {location}', 'Статус базы данных: исследовано', 'Практическое применение: после исследования этот вид можно высаживать в построенные грядки и горшки.'])
        elif isinstance(entity, AnimalType):
            lines.extend(['Класс: живое существо', f'Поведение: {self._behavior_name(entity.behavior)}', f'Здоровье: {entity.hp}', f'Урон при атаке: {entity.damage}', f'Скорость: {entity.speed:.3f}', f"Съедобность: {('да' if entity.edible else 'нет')}", f'Объём в инвентаре: {entity.volume}', f'Обнаружено: {location}'])
        else:
            if getattr(entity, 'is_buildable', False):
                category = 'постройка / устройство'
            elif getattr(entity, 'is_weapon', False):
                category = 'оружие'
            elif getattr(entity, 'edible', False):
                category = 'съедобный предмет / ресурс'
            else:
                category = 'предмет / ресурс'
            lines.extend([f'Класс: {category}', f"Размер: {getattr(entity, 'size', 0)}px", f"Объём в инвентаре: {getattr(entity, 'volume', 1)}", f'Обнаружено: {location}'])
            if getattr(entity, 'is_weapon', False):
                lines.append(f'Урон: {entity.damage}')
                lines.append(f'Дальность: {entity.weapon_range}')
                lines.append(f"Тип оружия: {entity.weapon_type or 'не указан'}")
            if getattr(entity, 'edible', False):
                lines.append(f'Питательность: +{entity.hunger_value} голода')
                lines.append(f'Жидкость: +{entity.thirst_value} жажды')
            if entity in self._planter_types():
                lines.append('Контейнер для выращивания: ПКМ без выбранного сканера открывает посадку.')
        return lines

    def _scanner_preview_source(self, target):
        entity = target['entity']
        cache_key = (target['kind'], id(entity))
        cached = self.scanner_preview_cache.get(cache_key)
        if cached is not None:
            return cached
        source = None
        if target['kind'] == 'golem':
            source = generate_golem_sprite(entity.kind, 340, shattered=target['colony'].phase == 'rubble')
        elif target['kind'] == 'bacteria':
            source = generate_golem_bacterium_preview(entity.kind, 290)
        elif target['kind'] == 'moss':
            source = generate_mad_moss_sprite(270)
        elif target['kind'] == 'tree':
            source = pygame.transform.smoothscale(generate_tree_sprite(TREE_SPECIES.index(entity.name)), (260, 260))
        elif isinstance(entity, FloraType):
            source = generate_subnautica_flora_sprite(entity.kind, 260, seed=entity.seed)
        elif isinstance(entity, AnimalType):
            if entity.sprite_kind is not None:
                length = 420
                width = max(75, int(length * entity.fish_width / max(1.0, entity.fish_length)))
                source = generate_subnautica_creature_sprite(entity.sprite_kind, length, width)
            elif entity.image is not None:
                source = entity.image.copy()
        elif getattr(entity, 'is_buildable', False):
            source = generate_building_sprite(entity.name, 220, entity.color, wall=getattr(entity, 'is_wall', False))
        else:
            source = generate_highres_resource_preview(entity, 280)
            if source is None and getattr(entity, 'image', None) is not None:
                source = entity.image.copy()
        if source is None:
            source = pygame.Surface((180, 180), pygame.SRCALPHA)
            if getattr(entity, 'shape', 'rect') == 'circle':
                pygame.draw.circle(source, entity.color, (90, 90), 70)
            else:
                pygame.draw.rect(source, entity.color, (25, 25, 130, 130), border_radius=18)
        self.scanner_preview_cache[cache_key] = source
        return source

    @staticmethod
    def _fit_surface(source, max_width, max_height):
        sw, sh = source.get_size()
        if sw <= 0 or sh <= 0:
            return source
        scale = min(max_width / sw, max_height / sh)
        nw = max(1, int(sw * scale))
        nh = max(1, int(sh * scale))
        return pygame.transform.smoothscale(source, (nw, nh))

    def scan_with_scanner(self):
        if not self._is_scanner_selected():
            return False
        target = self._scan_target_at_cursor()
        if target is None:
            self.notification = 'Сканер: наведите прицел на предмет, ресурс, существо или растение'
            self.notification_timer = 100
            return False
        entity = target['entity']
        entry_key = (target['kind'], entity.name)
        first_scan = entry_key not in self.scanned_entries
        self.scanned_entries.add(entry_key)
        if isinstance(entity, FloraType):
            self.researched_flora.add(entity.kind)
        target['first_scan'] = first_scan
        target['details'] = self._scanner_details(target)
        self.scanner_scroll_offset = 0
        self.scanner_scroll_max = 0
        self.scanner_target = target
        self.state = 'scanner'
        self.notification = ''
        self.notification_timer = 0
        return True

    def _planter_types(self):
        return (exterior_growbed, indoor_growbed, plant_pot, composite_pot, chic_pot, plant_shelf)

    def _planter_capacity(self, planter):
        if planter is exterior_growbed:
            return 6
        if planter is indoor_growbed:
            return 4
        if planter is plant_shelf:
            return 2
        return 1

    def _planter_key(self, x, y):
        return (bool(self.in_cave), int(x), int(y))

    def open_planting_menu_at_cursor(self):
        x = int(math.floor(self.mouse_world_x))
        y = int(math.floor(self.mouse_world_y))
        obj = self._get_current_object_grid().get((x, y))
        if obj not in self._planter_types():
            return False
        researched = [flora for flora in flora_types.values() if flora.kind in self.researched_flora]
        researched.sort(key=lambda flora: flora.name.lower())
        if not researched:
            self.notification = 'Сначала исследуйте растение сканером'
            self.notification_timer = 100
            return True
        self.planting_target = {'x': x, 'y': y, 'planter': obj}
        self.planting_page = 0
        self.state = 'planting'
        return True

    def _researched_plant_list(self):
        plants = [flora for flora in flora_types.values() if flora.kind in self.researched_flora]
        plants.sort(key=lambda flora: flora.name.lower())
        return plants

    def plant_researched_flora(self, visible_index):
        if self.state != 'planting' or self.planting_target is None:
            return
        plants = self._researched_plant_list()
        index = self.planting_page * self.plants_per_page + visible_index
        if not 0 <= index < len(plants):
            return
        x = self.planting_target['x']
        y = self.planting_target['y']
        planter = self.planting_target['planter']
        key = self._planter_key(x, y)
        planted = self.planted_flora.setdefault(key, [])
        capacity = self._planter_capacity(planter)
        if len(planted) >= capacity:
            self.notification = 'В этой грядке/горшке больше нет свободных мест'
            self.notification_timer = 100
            return
        flora = plants[index]
        if flora.kind not in self.researched_flora:
            self.notification = 'Этот вид ещё не исследован'
            self.notification_timer = 100
            return
        planted.append(flora)
        self.notification = f'Посажено: {flora.name} ({len(planted)}/{capacity})'
        self.notification_timer = 100

    def _draw_planted_flora_at(self, x, y, screen_x, screen_y):
        plants = self.planted_flora.get(self._planter_key(x, y), ())
        if not plants:
            return
        offsets = [(0, 1), (-8, -3), (8, -3), (-8, 7), (8, 7), (0, -8), (0, 9)]
        for index, flora in enumerate(plants):
            image = flora.image
            if image is None:
                continue
            max_size = 20 if len(plants) <= 2 else 15
            fitted = self._fit_surface(image, max_size, max_size)
            ox, oy = offsets[index % len(offsets)]
            rect = fitted.get_rect(center=(screen_x + CELL_SIZE // 2 + ox, screen_y + CELL_SIZE // 2 + oy))
            self.screen.blit(fitted, rect)

    def _draw_wrapped_lines(self, lines, x, y, max_width, color=(220, 235, 240), line_height=22):
        cursor_y = y
        for source_line in lines:
            words = str(source_line).split()
            current = ''
            wrapped = []
            for word in words:
                test = word if not current else current + ' ' + word
                if self.small_font.size(test)[0] <= max_width:
                    current = test
                else:
                    if current:
                        wrapped.append(current)
                    current = word
            if current:
                wrapped.append(current)
            if not wrapped:
                wrapped = ['']
            for line in wrapped:
                surf = self.small_font.render(line, True, color)
                self.screen.blit(surf, (x, cursor_y))
                cursor_y += line_height
            cursor_y += 3
        return cursor_y

    def draw_scanner_view(self):
        if self.scanner_target is None:
            self.state = 'playing'
            return
        target = self.scanner_target
        entity = target['entity']
        self.screen.fill((4, 20, 28))
        margin = 28
        header_h = 70
        pygame.draw.rect(self.screen, (8, 55, 68), (margin, margin, SCREEN_SIZE - margin * 2, SCREEN_SIZE - margin * 2), border_radius=14)
        pygame.draw.rect(self.screen, (60, 210, 220), (margin, margin, SCREEN_SIZE - margin * 2, SCREEN_SIZE - margin * 2), 2, border_radius=14)
        title = self.large_font.render(entity.name, True, (170, 250, 250))
        self.screen.blit(title, (margin + 22, margin + 16))
        status_text = 'НОВАЯ ЗАПИСЬ' if target.get('first_scan') else 'ИССЛЕДОВАНО'
        status = self.small_font.render(status_text, True, (130, 255, 170))
        self.screen.blit(status, (SCREEN_SIZE - margin - status.get_width() - 22, margin + 25))
        content_top = margin + header_h
        preview_w = int((SCREEN_SIZE - margin * 2) * 0.46)
        preview_rect = pygame.Rect(margin + 18, content_top + 10, preview_w - 28, SCREEN_SIZE - content_top - margin - 60)
        pygame.draw.rect(self.screen, (3, 26, 35), preview_rect, border_radius=12)
        pygame.draw.rect(self.screen, (35, 130, 150), preview_rect, 2, border_radius=12)
        preview = self._fit_surface(self._scanner_preview_source(target), preview_rect.width - 30, preview_rect.height - 30)
        preview_pos = preview.get_rect(center=preview_rect.center)
        self.screen.blit(preview, preview_pos)
        scan_y = preview_rect.top + self.frame_count * 3 % max(1, preview_rect.height)
        pygame.draw.line(self.screen, (90, 245, 245), (preview_rect.left + 6, scan_y), (preview_rect.right - 6, scan_y), 1)
        info_x = preview_rect.right + 24
        info_width = SCREEN_SIZE - margin - info_x - 20
        heading = self.font.render('Подробная информация', True, (255, 235, 150))
        self.screen.blit(heading, (info_x, content_top + 14))
        clip_rect = pygame.Rect(info_x, content_top + 42, info_width, SCREEN_SIZE - margin - 78 - (content_top + 42))
        original_clip = self.screen.get_clip()
        self.screen.set_clip(clip_rect)
        text_end = self._draw_wrapped_lines(target.get('details', ()), info_x, content_top + 48 - self.scanner_scroll_offset, info_width)
        self.screen.set_clip(original_clip)
        self.scanner_scroll_max = max(0, text_end + self.scanner_scroll_offset - clip_rect.bottom + 12)
        self.scanner_scroll_offset = min(self.scanner_scroll_offset, self.scanner_scroll_max)
        footer = self.small_font.render('Колесо — листать | ESC или ПКМ — закрыть', True, (170, 210, 215))
        self.screen.blit(footer, footer.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE - margin - 20)))

    def draw_planting_menu(self):
        if self.planting_target is None:
            self.state = 'playing'
            return
        overlay = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 205))
        self.screen.blit(overlay, (0, 0))
        plants = self._researched_plant_list()
        max_page = max(0, (len(plants) - 1) // self.plants_per_page)
        self.planting_page = min(self.planting_page, max_page)
        start = self.planting_page * self.plants_per_page
        shown = plants[start:start + self.plants_per_page]
        x = self.planting_target['x']
        y = self.planting_target['y']
        planter = self.planting_target['planter']
        key = self._planter_key(x, y)
        planted = self.planted_flora.get(key, ())
        capacity = self._planter_capacity(planter)
        title = self.large_font.render(f'{planter.name}: посадка растений', True, (150, 245, 190))
        self.screen.blit(title, title.get_rect(center=(SCREEN_SIZE // 2, 60)))
        occupancy = self.small_font.render(f'Занято мест: {len(planted)}/{capacity}. Доступны только исследованные сканером виды.', True, (225, 225, 225))
        self.screen.blit(occupancy, occupancy.get_rect(center=(SCREEN_SIZE // 2, 96)))
        start_y = 130
        row_h = 45
        for i, flora in enumerate(shown):
            row_y = start_y + i * row_h
            pygame.draw.rect(self.screen, (15, 55, 52), (105, row_y, SCREEN_SIZE - 210, row_h - 5), border_radius=8)
            pygame.draw.rect(self.screen, (55, 140, 110), (105, row_y, SCREEN_SIZE - 210, row_h - 5), 1, border_radius=8)
            number = self.font.render(f'{i + 1}.', True, (255, 235, 135))
            self.screen.blit(number, (120, row_y + 9))
            icon = self._fit_surface(flora.image, 30, 30)
            self.screen.blit(icon, icon.get_rect(center=(170, row_y + 20)))
            label = self.font.render(flora.name, True, (225, 245, 235))
            self.screen.blit(label, (195, row_y + 9))
        footer = self.small_font.render(f'1–9 — посадить | ← → — страница {self.planting_page + 1}/{max_page + 1} | ESC — закрыть', True, (205, 220, 220))
        self.screen.blit(footer, footer.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE - 35)))

    def _apply_repulsion(self, radius=5.0, push=1.6):
        affected = 0
        for animal in self.get_current_animals():
            if animal.hp <= 0 or getattr(animal, 'is_companion', False):
                continue
            dx = animal.x - self.player_x
            dy = animal.y - self.player_y
            dist_sq = dx * dx + dy * dy
            if dist_sq <= 0 or dist_sq > radius * radius:
                continue
            dist = math.sqrt(dist_sq)
            old_x, old_y = (animal.x, animal.y)
            candidate_x = animal.x + dx / dist * push
            candidate_y = animal.y + dy / dist * push
            if animal.home_biome.contains(candidate_x, candidate_y):
                animal.x = candidate_x
                animal.y = candidate_y
            animal.attack_timer = max(animal.attack_timer, 60)
            animal._update_rotation()
            if not self.in_cave:
                self.world.update_animal_index(animal, old_x, old_y)
            affected += 1
        return affected

    def _use_special_item(self, item):
        if item is antibiotic_syringe:
            return self._inject_antibiotic()
        if item is first_aid_kit:
            heal = 12
            self.player_hp = min(self.max_player_hp, self.player_hp + heal)
            self._consume_selected_item(item)
            self.notification = f'Аптечка использована. HP: {self.player_hp}/{self.max_player_hp}'
            self.notification_timer = 90
            return True
        if item is flashlight_type:
            self.flashlight_on = not self.flashlight_on
            self.notification = 'Фонарик включён' if self.flashlight_on else 'Фонарик выключен'
            self.notification_timer = 60
            return True
        if item is seaglide_type:
            self.seaglide_on = not self.seaglide_on
            self.notification = 'Морской глайдер активирован' if self.seaglide_on else 'Морской глайдер выключен'
            self.notification_timer = 60
            return True
        if item is flare_type:
            self.active_flare_timer = max(self.active_flare_timer, 18 * 60)
            self._consume_selected_item(item)
            self.notification = 'Ракета горит: ночью она не отпугивает хищников' if self._is_dark_night() else 'Ракета горит: часть хищников отступает'
            self.notification_timer = 150
            return True
        if item is fire_extinguisher:
            self.damage_resist_timer = max(self.damage_resist_timer, 20 * 60)
            self._consume_selected_item(item)
            self.notification = 'Огнетушитель использован: временно снижает получаемый урон'
            self.notification_timer = 120
            return True
        if item is air_bladder:
            self.speed_boost_timer = max(self.speed_boost_timer, 8 * 60)
            self.player_thirst = min(self.max_thirst, self.player_thirst + 10)
            self._consume_selected_item(item)
            self.notification = 'Воздушный пузырь использован: краткий рывок и восстановление жажды'
            self.notification_timer = 120
            return True
        if item is scanner_type:
            self.notification = 'Сканер выбран: наведите прицел на цель и нажмите ПКМ'
            self.notification_timer = 120
            return True
        if item is pathfinder_tool:
            best = self._find_nearest_path_target()
            if best is None:
                self.notification = 'Инструмент исследователя: ориентир не найден'
            else:
                _, x, y, label = best
                self.notification = f'Инструмент исследователя: ближайший ориентир — {label} ({self._direction_hint(x, y)})'
            self.notification_timer = 120
            return True
        if item is repair_tool:
            healed = min(self.max_player_hp - self.player_hp, 5)
            if healed <= 0:
                self.notification = 'Ремонтный инструмент: скафандр не требует обслуживания'
            else:
                self.player_hp += healed
                self.notification = f'Ремонтный инструмент обслужил скафандр. HP +{healed}'
            self.notification_timer = 90
            return True
        if item is propulsion_cannon:
            grid = self._get_current_object_grid()
            best = None
            for (x, y), obj in grid.items():
                if getattr(obj, 'is_buildable', False) or getattr(obj, 'is_weapon', False):
                    continue
                dx = x - self.player_x
                dy = y - self.player_y
                dist_sq = dx * dx + dy * dy
                if dist_sq <= 16 and (best is None or dist_sq < best[0]):
                    best = (dist_sq, x, y, obj)
            if best is None:
                self.notification = 'Пропульсионная пушка: поблизости нет подходящего предмета'
            else:
                _, x, y, obj = best
                if self.add_to_inventory(obj):
                    grid.pop((x, y), None)
                    self.notification = f'Пропульсионная пушка притянула: {obj.name}'
                else:
                    self.notification = 'Нет места в инвентаре'
            self.notification_timer = 90
            return True
        if item is repulsion_cannon:
            affected = self._apply_repulsion(radius=5.5, push=1.9)
            self.notification = f'Репульсионная пушка отбросила существ: {affected}'
            self.notification_timer = 90
            return True
        if item is stasis_rifle:
            frozen = 0
            for animal in self.get_current_animals():
                if animal.hp <= 0 or getattr(animal, 'is_companion', False):
                    continue
                dx = animal.x - self.player_x
                dy = animal.y - self.player_y
                dist_sq = dx * dx + dy * dy
                if dist_sq <= 36:
                    animal.stasis_timer = max(getattr(animal, 'stasis_timer', 0), 6 * 60)
                    frozen += 1
                    if frozen >= 3:
                        break
            self.notification = f'Стазис-винтовка обездвижила существ: {frozen}'
            self.notification_timer = 90
            return True
        if item is laser_cutter:
            hit = False
            for animal in self.get_current_animals():
                if animal.hp <= 0 or getattr(animal, 'is_companion', False):
                    continue
                dx = animal.x - self.player_x
                dy = animal.y - self.player_y
                dist_sq = dx * dx + dy * dy
                if dist_sq <= 9:
                    animal.hp -= 18
                    self.notification = f'Лазерный резак повредил {animal.type.name} (HP: {animal.hp})'
                    self.notification_timer = 90
                    if animal.hp <= 0:
                        self.handle_animal_death(animal)
                    hit = True
                    break
            if not hit and (not self.in_cave):
                golems = self._near_golems(self.player_x, self.player_y, 3.0, ('active',))
                if golems:
                    self._damage_golem(min(golems, key=lambda g: (g.x - self.player_x) ** 2 + (g.y - self.player_y) ** 2), 18)
                    hit = True
            if not hit and (not self.in_cave):
                mosses = self._moss_near(self.player_x, self.player_y, 3.0)
                if mosses:
                    self._hit_mad_moss(min(mosses, key=lambda m: (m.x - self.player_x) ** 2 + (m.y - self.player_y) ** 2), 18)
                    hit = True
            if not hit:
                self.notification = 'Лазерный резак: цель слишком далеко'
                self.notification_timer = 90
            return True
        if item is creature_decoy:
            self.active_decoys.append({'x': float(self.player_x), 'y': float(self.player_y), 'timer': 14 * 60, 'in_cave': self.in_cave})
            self._consume_selected_item(item)
            self.notification = 'Приманка для существ выброшена'
            self.notification_timer = 90
            return True
        return False

    def _base_light_bonus(self):
        bonus = 0
        if self.seaglide_on:
            bonus += 12
        if self.active_flare_timer > 0:
            bonus += 28
        return min(55, bonus)

    def _update_base_effects(self):
        self.frame_count += 1
        if self.active_flare_timer > 0:
            self.active_flare_timer -= 1
        if self.speed_boost_timer > 0:
            self.speed_boost_timer -= 1
        if self.damage_resist_timer > 0:
            self.damage_resist_timer -= 1
        if self.scanner_highlight_timer > 0:
            self.scanner_highlight_timer -= 1
            if self.scanner_highlight_timer <= 0:
                self.scanner_highlight_cells.clear()
        if self.active_decoys:
            for decoy in self.active_decoys:
                decoy['timer'] -= 1
            self.active_decoys = [d for d in self.active_decoys if d['timer'] > 0]
        if self.frame_count % 15 == 0:
            grid = self._get_current_object_grid()
            traps = [(x, y) for (x, y), obj in list(grid.items()) if obj is grav_trap]
            for tx, ty in traps:
                moved = []
                for (x, y), obj in list(grid.items()):
                    if (x, y) == (tx, ty):
                        continue
                    if getattr(obj, 'is_buildable', False) or getattr(obj, 'is_weapon', False):
                        continue
                    dx = tx - x
                    dy = ty - y
                    if dx * dx + dy * dy > 9:
                        continue
                    step_x = 0 if dx == 0 else 1 if dx > 0 else -1
                    step_y = 0 if dy == 0 else 1 if dy > 0 else -1
                    nx = x + step_x if abs(dx) >= abs(dy) else x
                    ny = y + step_y if abs(dy) > abs(dx) else y
                    if (nx, ny) == (tx, ty) or (nx, ny) not in grid:
                        moved.append(((x, y), (nx, ny), obj))
                for old_pos, new_pos, obj in moved:
                    if old_pos in grid and new_pos not in grid:
                        grid.pop(old_pos, None)
                        grid[new_pos] = obj
        if self.frame_count % 180 != 0:
            return
        nearby = self._nearby_buildables(radius=3.2)
        if not nearby:
            return
        nearby_types = {obj for obj, _x, _y, _d in nearby}
        if any((obj in nearby_types for obj in (bed_type, bench_type, chair_type))):
            self.player_hp = min(self.max_player_hp, self.player_hp + 1)
        if vending_machine in nearby_types:
            self.player_hunger = min(self.max_hunger, self.player_hunger + 2)
        if coffee_machine in nearby_types:
            self.player_thirst = min(self.max_thirst, self.player_thirst + 3)
        if water_filtration_machine in nearby_types:
            self.player_thirst = min(self.max_thirst, self.player_thirst + 2)
        if medical_fabricator in nearby_types and self.frame_count % 720 == 0:
            if self.add_to_inventory(first_aid_kit):
                self.notification = 'Изготовитель аптечек выдал новую аптечку'
                self.notification_timer = 90
        if scanner_room_type in nearby_types and self.frame_count % 540 == 0:
            self._scan_environment(passive=True)
        if radio_type in nearby_types and self.frame_count % 900 == 0:
            best = self._find_nearest_path_target()
            if best is not None:
                _, x, y, label = best
                self.notification = f'Радио: ориентир — {label} ({self._direction_hint(x, y)})'
                self.notification_timer = 90

    def use_item(self):
        if self.state not in ('playing', 'inventory'):
            return
        if self.active_slot_index == -1:
            self.notification = 'Выберите предмет в инвентаре'
            self.notification_timer = 60
            return
        item = self.inventory[self.active_slot_index]
        if item is None:
            self.notification = 'Слот пуст'
            self.notification_timer = 60
            return
        if item.edible:
            self._whale_quest_on_food()
            self.player_hunger = min(self.max_hunger, self.player_hunger + item.hunger_value)
            self.player_thirst = min(self.max_thirst, self.player_thirst + item.thirst_value)
            food_heal = 0 if item is mad_moss_item else 24 if item.name == WHALE_NAME else 2
            self.player_hp = min(self.max_player_hp, self.player_hp + food_heal)
            self._consume_selected_item(item)
            self.notification = f'Использовано: {item.name}. HP +{food_heal}, Голод: {self.player_hunger:.0f}, Жажда: {self.player_thirst:.0f}'
            self.notification_timer = 60
            return
        if self.state == 'inventory':
            return
        if self._use_special_item(item):
            return
        self.notification = 'У этого предмета пока нет активного применения'
        self.notification_timer = 60

    def _near_golems(self, x, y, radius=2.0, phases=None):
        if self.in_cave:
            return []
        rr = radius * radius
        return [g for g in self.world.golem_colonies if (phases is None or g.phase in phases) and (g.x - x) ** 2 + (g.y - y) ** 2 <= rr]

    def _damage_golem(self, golem, damage):
        if not golem.hit(damage, self.frame_count):
            self.notification = 'Бактерии живы, но тело пока не собрано. Нужен антибиотик.'
        elif golem.phase == 'rubble':
            self.notification = GOLEM_DATA[golem.kind]['name'] + ' разрушен! Бактерии выжили и могут собрать его снова.'
        else:
            self.notification = '%s: %d/%d HP' % (GOLEM_DATA[golem.kind]['name'], golem.hp, golem.max_hp)
        self.notification_timer = 130
        self._map_biome_species_cache = None

    def _inject_antibiotic(self):
        targets = self._near_golems(self.player_x, self.player_y, 2.0, ('active', 'rubble', 'breeding'))
        if not targets:
            self.notification = 'Антибиотик: приблизьтесь к голему или заражённым обломкам (2 клетки)'
            self.notification_timer = 110
            return True
        golem = min(targets, key=lambda g: (g.x - self.player_x) ** 2 + (g.y - self.player_y) ** 2)
        self.world.golem_colonies.remove(golem)
        self._consume_selected_item(antibiotic_syringe)
        self._map_biome_species_cache = None
        self.notification = GOLEM_DATA[golem.kind]['name'] + ': колония уничтожена антибиотиком. Воскреснуть больше некому!'
        self.notification_timer = 180
        return True

    def _update_golems(self):
        if self.in_cave or not GOLEM_ENABLED:
            return
        radius = max(16, self._render_view_radius + 7)
        for g in self.world.golem_colonies:
            if abs(g.x - self.player_x) > radius or abs(g.y - self.player_y) > radius:
                continue
            g.update(self.world, self.player_x, self.player_y, self.frame_count, self._selected_inventory_item() is antibiotic_syringe)
            if g.phase != 'active' or self.frame_count < g.next_attack:
                continue
            d = math.hypot(g.x - self.player_x, g.y - self.player_y)
            if d > 1.22:
                continue
            g.next_attack = self.frame_count + GOLEM_DAMAGE_COOLDOWN
            damage = GOLEM_DATA[g.kind]['damage']
            if g.kind == 'water':
                self.player_thirst = max(0, self.player_thirst - 3)
            elif g.kind == 'wood':
                damage += 1 if self.frame_count % 3 == 0 else 0
            elif g.kind == 'dung':
                self.player_hunger = max(0, self.player_hunger - 4)
            self._damage_player_or_vehicle(damage)
            self.notification = '%s атакует! −%d HP' % (GOLEM_DATA[g.kind]['name'], damage)
            self.notification_timer = 65

    def _draw_golems(self, camera_x, camera_y):
        if self.in_cave:
            return
        rr = self._render_view_radius + 3
        for x, y in self.world.golem_dung_piles:
            if abs(x - self.player_x) > rr or abs(y - self.player_y) > rr:
                continue
            sx = round((x - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = round((y - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            pygame.draw.ellipse(self.screen, (81, 63, 36), (sx - 9, sy - 6, 18, 12))
            pygame.draw.ellipse(self.screen, (135, 94, 48), (sx - 6, sy - 6, 12, 8))
        for g in self.world.golem_colonies:
            if abs(g.x - self.player_x) > rr or abs(g.y - self.player_y) > rr:
                continue
            if g.phase == 'spore':
                continue
            if g.phase == 'breeding' and g.mass < 20:
                continue
            sx = round((g.x - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = round((g.y - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            mult = max(0.28, min(1.0, g.mass / GOLEM_FORMATION_MASS)) if g.phase == 'breeding' else 1.0
            diameter = round(CELL_SIZE * GOLEM_DATA[g.kind]['diameter'] * mult)
            cache_key = (g.kind, diameter, g.phase == 'rubble')
            sprite = _GOLEM_SPRITE_CACHE.get(cache_key)
            if sprite is None:
                sprite = generate_golem_sprite(g.kind, diameter, g.phase == 'rubble')
                if len(_GOLEM_SPRITE_CACHE) > 250:
                    _GOLEM_SPRITE_CACHE.clear()
                _GOLEM_SPRITE_CACHE[cache_key] = sprite
            self.screen.blit(sprite, sprite.get_rect(center=(sx, sy)))
            if g.phase == 'active' and g.hp < g.max_hp:
                bx = sx - 18
                by = sy - diameter // 2 - 8
                pygame.draw.rect(self.screen, (82, 32, 32), (bx, by, 36, 4))
                pygame.draw.rect(self.screen, (55, 224, 151), (bx, by, round(36 * g.hp / g.max_hp), 4))
            elif g.phase == 'rubble':
                pygame.draw.circle(self.screen, (235, 181, 57), (sx, sy), max(7, diameter // 2 + 4), 1)

    def _moss_near(self, x, y, radius=1.0):
        if self.in_cave:
            return []
        rr = radius * radius
        return [m for m in self.world.moss_colonies if (m.x - x) ** 2 + (m.y - y) ** 2 <= rr]

    def _update_mad_moss(self):
        if self.in_cave:
            return
        radius = self._render_view_radius + 3
        for moss in self.world.moss_colonies:
            if abs(moss.x - self.player_x) <= radius and abs(moss.y - self.player_y) <= radius:
                moss.update(self.world)

    def _hit_mad_moss(self, moss, damage=1):
        """Любой удар делит одну крупную колонию на две меньшие."""
        if moss.terminal:
            self.notification = 'Мох измельчён до 1/5 клетки: подберите E'
            self.notification_timer = 95
            return True
        try:
            self.world.moss_colonies.remove(moss)
        except ValueError:
            return False
        child_diameter = max(MAD_MOSS_MIN_DIAMETER, moss.diameter / math.sqrt(2))
        terminal = child_diameter <= MAD_MOSS_MIN_DIAMETER + 0.028
        if terminal:
            child_diameter = MAD_MOSS_MIN_DIAMETER
        for sign in (-1, 1):
            spread = max(0.11, child_diameter * 0.47)
            angle = moss.heading + sign * math.pi * 0.5
            x = moss.x + math.cos(angle) * spread
            y = moss.y + math.sin(angle) * spread
            if self.world.get_biome_at(x, y) is not moss.home_biome:
                x, y = (moss.x, moss.y)
            part = MadMossColony(x, y, moss.home_biome, child_diameter, terminal)
            part.heading = moss.heading + sign * 0.95
            self.world.moss_colonies.append(part)
        self.notification = 'Бешеный мох разделился: 2 части' if not terminal else 'Мох разделился на 2 съедобные части (E)'
        self.notification_timer = 80
        return True

    def _draw_mad_moss(self, camera_x, camera_y, surface=None, zoom=1.0):
        if self.in_cave:
            return
        target = surface if surface is not None else self.screen
        extra = self._render_view_radius + 3
        for moss in self.world.moss_colonies:
            if abs(moss.x - self.player_x) > extra or abs(moss.y - self.player_y) > extra:
                continue
            if zoom == 1.0:
                sx = round((moss.x - camera_x) * CELL_SIZE + CELL_SIZE // 2)
                sy = round((moss.y - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            else:
                sx = round(SCREEN_SIZE * 0.5 + (moss.x - self.player_x) * CELL_SIZE * zoom)
                sy = round(SCREEN_SIZE * 0.5 + (moss.y - self.player_y) * CELL_SIZE * zoom)
            diameter = moss.diameter * CELL_SIZE * zoom
            sprite = get_mad_moss_sprite(diameter, moss.variant)
            target.blit(sprite, sprite.get_rect(center=(sx, sy)))

    def pick_up(self):
        player_x = int(self.player_x)
        player_y = int(self.player_y)
        if not self.in_cave:
            nearest = sorted(self._moss_near(self.player_x, self.player_y, 0.72), key=lambda m: (m.x - self.player_x) ** 2 + (m.y - self.player_y) ** 2)
            if nearest:
                moss = nearest[0]
                if not moss.terminal:
                    self.notification = 'Сначала измельчите Бешеный мох ударом (Пробел)!'
                    self.notification_timer = 90
                    return
                if self.add_to_inventory(mad_moss_item):
                    self.world.moss_colonies.remove(moss)
                    self.notification = 'Подобран Бешеный мох (+1 еда, +4 вода при поедании)'
                else:
                    self.notification = 'Инвентарь полон — мох остался на земле'
                self.notification_timer = 90
                return
        dropped = self._current_dropped_items()
        dropped_stack = dropped.get((player_x, player_y))
        if dropped_stack:
            item = dropped_stack[-1]
            if self.add_to_inventory(item):
                dropped_stack.pop()
                if not dropped_stack:
                    dropped.pop((player_x, player_y), None)
                self.notification = f'Подобрано: {item.name}'
                self.notification_timer = 40
            else:
                self.notification = 'Нет места в инвентаре!'
                self.notification_timer = 60
            return
        if self.in_cave:
            obj = self.cave_world.get_object_at(player_x, player_y)
        else:
            obj = self.world.get_object_at(player_x, player_y)
        if obj is not None and (not obj.is_buildable):
            if self.add_to_inventory(obj):
                if self.in_cave:
                    self.cave_world.remove_object_at(player_x, player_y)
                else:
                    self.world.remove_object_at(player_x, player_y)
                self.notification = f'Подобрано: {obj.name}'
                self.notification_timer = 40
            else:
                self.notification = 'Нет места в инвентаре!'
                self.notification_timer = 60
            return
        animals = self.get_current_animals()
        for animal in list(animals):
            if getattr(animal, 'is_companion', False):
                continue
            if int(animal.x) != player_x or int(animal.y) != player_y:
                continue
            if animal.hp > 0 and animal.type.behavior == 'predator':
                self.notification = 'Хищник жив, убейте его!'
                self.notification_timer = 60
                return
            if self.add_to_inventory(animal.type, wild_catch=True):
                self.remove_current_animal(animal)
                self.notification = f'Подобран {animal.type.name}'
                self.notification_timer = 60
            else:
                self.notification = 'Нет места в инвентаре!'
                self.notification_timer = 60
            return

    def build(self):
        if self.state != 'playing':
            return
        if self.active_slot_index == -1:
            self.notification = 'Выберите предмет в инвентаре'
            self.notification_timer = 60
            return
        item = self.inventory[self.active_slot_index]
        if item is None:
            self.notification = 'Слот пуст'
            self.notification_timer = 60
            return
        if not item.is_buildable:
            self.notification = 'Этот предмет нельзя установить'
            self.notification_timer = 60
            return
        if getattr(item, 'requires_habitat_builder', False) and self.inventory_counts.get(habitat_builder_type, 0) <= 0:
            self.notification = 'Для установки нужен Строитель'
            self.notification_timer = 90
            return
        player_x = int(self.player_x)
        player_y = int(self.player_y)
        active_world = self.cave_world if self.in_cave else self.world
        if active_world.get_object_at(player_x, player_y) is not None:
            self.notification = 'Клетка занята'
            self.notification_timer = 60
            return
        active_world.object_grid[player_x, player_y] = item
        self.inventory[self.active_slot_index] = None
        self.inventory_counts[item] -= 1
        if self.inventory_counts[item] <= 0:
            del self.inventory_counts[item]
        self.active_slot_index = -1
        self.notification = f'Установлено: {item.name}'
        self.notification_timer = 60
        if item.is_wall:
            self.notification = 'Стена установлена под игроком'
            self.notification_timer = 120

    def destroy_build(self):
        if self.state != 'playing':
            return
        player_x = int(self.player_x)
        player_y = int(self.player_y)
        active_world = self.cave_world if self.in_cave else self.world
        obj = active_world.get_object_at(player_x, player_y)
        if obj is None or not obj.is_buildable:
            self.notification = 'Здесь нет постройки'
            self.notification_timer = 60
            return
        active_world.remove_object_at(player_x, player_y)
        self.planted_flora.pop((bool(self.in_cave), player_x, player_y), None)
        if self.add_to_inventory(obj):
            self.notification = f'Разрушено: {obj.name}'
        else:
            self.notification = 'Инвентарь полон. Предмет потерян.'
        self.notification_timer = 60

    def attack(self):
        if self.state != 'playing':
            return
        player_x = self.player_x
        player_y = self.player_y
        max_distance_squared = self.player_attack_range ** 2
        if not self.in_cave:
            candidates = self._moss_near(player_x, player_y, self.player_attack_range)
            if candidates:
                moss = min(candidates, key=lambda m: (m.x - player_x) ** 2 + (m.y - player_y) ** 2)
                self._hit_mad_moss(moss, self.player_attack_damage)
                return
        if not self.in_cave:
            targets = self._near_golems(player_x, player_y, self.player_attack_range, ('active',))
            if targets:
                target = min(targets, key=lambda g: (g.x - player_x) ** 2 + (g.y - player_y) ** 2)
                self._damage_golem(target, self.player_attack_damage)
                return
        for animal in list(self.get_current_animals()):
            if animal.hp <= 0:
                continue
            if getattr(animal, 'is_companion', False):
                continue
            dx = animal.x - player_x
            dy = animal.y - player_y
            distance_squared = dx * dx + dy * dy
            if distance_squared > max_distance_squared:
                continue
            animal.hp -= self.player_attack_damage
            self.notification = f'Атакован {animal.type.name}, HP: {animal.hp}'
            self.notification_timer = 30
            if animal.hp <= 0:
                self.handle_animal_death(animal)
            break

    def shoot(self, target_x, target_y):
        if self.state != 'playing':
            return
        weapon = self.equipped_weapon
        if weapon is None:
            self.notification = 'Нет экипированного оружия!'
            self.notification_timer = 60
            return
        target_dx = target_x - self.player_x
        target_dy = target_y - self.player_y
        target_distance_squared = target_dx ** 2 + target_dy ** 2
        if target_distance_squared <= 0:
            return
        target_length = math.sqrt(target_distance_squared)
        target_dx /= target_length
        target_dy /= target_length
        best_animal = None
        best_distance_squared = float('inf')
        max_range_squared = weapon.weapon_range ** 2
        cos_threshold = math.cos(math.radians(30))
        for animal in self.get_current_animals():
            if animal.hp <= 0:
                continue
            if getattr(animal, 'is_companion', False):
                continue
            dx = animal.x - self.player_x
            dy = animal.y - self.player_y
            distance_squared = dx * dx + dy * dy
            if distance_squared > max_range_squared:
                continue
            if distance_squared <= 0:
                continue
            distance = math.sqrt(distance_squared)
            direction_x = dx / distance
            direction_y = dy / distance
            dot = direction_x * target_dx + direction_y * target_dy
            if dot >= cos_threshold and distance_squared < best_distance_squared:
                best_distance_squared = distance_squared
                best_animal = animal
        if not self.in_cave:
            for moss in self._moss_near(self.player_x, self.player_y, weapon.weapon_range):
                dx, dy = (moss.x - self.player_x, moss.y - self.player_y)
                dist_sq = dx * dx + dy * dy
                if not 0 < dist_sq < best_distance_squared:
                    continue
                if (dx * target_dx + dy * target_dy) / math.sqrt(dist_sq) >= cos_threshold:
                    best_animal = None
                    best_distance_squared = dist_sq
                    best_moss = moss
            if 'best_moss' in locals():
                self._hit_mad_moss(best_moss, weapon.damage)
                return
        if not self.in_cave:
            possible = self._near_golems(self.player_x, self.player_y, weapon.weapon_range, ('active',))
            chosen = None
            chosen_dist = best_distance_squared
            for g in possible:
                ddx = g.x - self.player_x
                ddy = g.y - self.player_y
                dd2 = ddx * ddx + ddy * ddy
                if dd2 <= 0.01 or dd2 >= chosen_dist:
                    continue
                if (ddx * target_dx + ddy * target_dy) / math.sqrt(dd2) >= cos_threshold:
                    chosen = g
                    chosen_dist = dd2
            if chosen is not None:
                self._damage_golem(chosen, weapon.damage)
                return
        if best_animal is None:
            self.notification = 'Промах!'
            self.notification_timer = 30
            return
        best_animal.hp -= weapon.damage
        self.notification = f'{weapon.name} поразил {best_animal.type.name} (HP: {best_animal.hp})'
        self.notification_timer = 60
        if best_animal.hp <= 0:
            self.handle_animal_death(best_animal)

    def is_inside_start_capsule(self, x, y):
        x1, y1, x2, y2 = self.start_capsule_rect
        return x1 <= int(x) <= x2 and y1 <= int(y) <= y2

    def is_inside_capsule_crafting_area(self, x, y):
        x1, y1, x2, y2 = self.start_capsule_inner_rect
        return x1 <= int(x) <= x2 and y1 <= int(y) <= y2

    def is_start_capsule_wall(self, x, y):
        x = int(x)
        y = int(y)
        if not self.is_inside_start_capsule(x, y):
            return False
        if (x, y) in self.start_capsule_door_cells:
            return False
        x1, y1, x2, y2 = self.start_capsule_rect
        return x in (x1, x2) or y in (y1, y2)

    def is_start_capsule_passage(self, x, y):
        cell = (int(x), int(y))
        return self.is_inside_capsule_crafting_area(*cell) or cell in self.start_capsule_door_cells

    def get_available_crafting_stations(self):
        """Return crafting stations currently usable by the player."""
        stations = []
        player_x = int(self.player_x)
        player_y = int(self.player_y)
        active_world = self.cave_world if self.in_cave else self.world
        obj = active_world.get_object_at(player_x, player_y)
        if self.in_cave:
            if obj is floor_type:
                stations.append('Изготовитель')
        elif (self.is_inside_capsule_crafting_area(player_x, player_y)
              or (self.is_aurora_biome(self.world.get_biome_at(player_x, player_y))
                  and self.world.get_biome_at(player_x, player_y).passable)):
            stations.append('Изготовитель')
        elif obj is floor_type:
            stations.append('Изготовитель')
        station_name = getattr(obj, 'crafting_station_name', None)
        if station_name and station_name not in stations:
            stations.append(station_name)
        if self.inventory_counts.get(habitat_builder_type, 0) > 0:
            if 'Строитель' not in stations:
                stations.append('Строитель')
        return stations

    def get_active_crafting_station(self):
        stations = self.get_available_crafting_stations()
        if not stations:
            return None
        self.craft_station_index %= len(stations)
        return stations[self.craft_station_index]

    def get_crafting_location_name(self):
        return self.get_active_crafting_station()

    def get_current_station_recipes(self):
        station = self.get_active_crafting_station()
        if station is None:
            return []
        return [recipe for recipe in recipes if recipe.station == station]

    def can_craft_here(self):
        return bool(self.get_available_crafting_stations())

    def draw_start_capsule_cell(self, x, y, screen_x, screen_y):
        """Рисует детализированную клетку стартовой капсулы."""
        if not self.is_inside_start_capsule(x, y):
            return False
        x1, y1, x2, y2 = self.start_capsule_rect
        cell_rect = pygame.Rect(screen_x, screen_y, CELL_SIZE, CELL_SIZE)
        inner_rect = cell_rect.inflate(-4, -4)
        cell = (x, y)
        if self.is_inside_capsule_crafting_area(x, y):
            pygame.draw.rect(self.screen, (34, 58, 68), cell_rect)
            pygame.draw.rect(self.screen, (54, 82, 92), inner_rect)
            pygame.draw.rect(self.screen, (90, 118, 124), inner_rect, 1)
            pygame.draw.line(self.screen, (42, 68, 78), (screen_x + CELL_SIZE // 2, screen_y + 3), (screen_x + CELL_SIZE // 2, screen_y + CELL_SIZE - 3), 1)
            pygame.draw.line(self.screen, (42, 68, 78), (screen_x + 3, screen_y + CELL_SIZE // 2), (screen_x + CELL_SIZE - 3, screen_y + CELL_SIZE // 2), 1)
            for rx, ry in ((6, 6), (CELL_SIZE - 7, 6), (6, CELL_SIZE - 7), (CELL_SIZE - 7, CELL_SIZE - 7)):
                pygame.draw.circle(self.screen, (150, 170, 174), (screen_x + rx, screen_y + ry), 1)
            if y == y1 + 1 and x in (x1 + 2, x2 - 2):
                pygame.draw.rect(self.screen, (15, 45, 58), (screen_x + 7, screen_y + 5, CELL_SIZE - 14, 6), border_radius=2)
                pygame.draw.rect(self.screen, (70, 220, 240), (screen_x + 9, screen_y + 7, CELL_SIZE - 18, 2))
            if x == x1 + 1 and y in (y1 + 2, y2 - 2):
                pygame.draw.rect(self.screen, (70, 78, 82), (screen_x + 4, screen_y + 6, 5, CELL_SIZE - 12), border_radius=2)
                pygame.draw.circle(self.screen, (80, 255, 150), (screen_x + 6, screen_y + CELL_SIZE // 2), 2)
            return True
        if cell in self.start_capsule_door_cells:
            pygame.draw.rect(self.screen, (44, 52, 58), cell_rect)
            pygame.draw.rect(self.screen, (105, 116, 120), inner_rect, 2)
            pygame.draw.line(self.screen, (230, 150, 45), (screen_x + 4, screen_y + 6), (screen_x + CELL_SIZE - 4, screen_y + CELL_SIZE - 6), 3)
            pygame.draw.line(self.screen, (40, 45, 48), (screen_x + 4, screen_y + 12), (screen_x + CELL_SIZE - 4, screen_y + CELL_SIZE - 0), 3)
            pygame.draw.circle(self.screen, (80, 235, 255), (screen_x + CELL_SIZE // 2, screen_y + 5), 2)
            return True
        pygame.draw.rect(self.screen, (126, 139, 143), cell_rect)
        pygame.draw.rect(self.screen, (74, 86, 90), inner_rect)
        pygame.draw.rect(self.screen, (177, 188, 190), cell_rect, 2)
        pygame.draw.rect(self.screen, (36, 47, 52), inner_rect, 1)
        pygame.draw.line(self.screen, (105, 118, 122), (screen_x + 4, screen_y + CELL_SIZE // 2), (screen_x + CELL_SIZE - 4, screen_y + CELL_SIZE // 2), 1)
        pygame.draw.circle(self.screen, (210, 218, 220), (screen_x + 5, screen_y + 5), 1)
        pygame.draw.circle(self.screen, (210, 218, 220), (screen_x + CELL_SIZE - 6, screen_y + CELL_SIZE - 6), 1)
        if y == y1 and x in (x1 + 2, x1 + 5):
            pygame.draw.ellipse(self.screen, (18, 45, 62), (screen_x + 6, screen_y + 8, CELL_SIZE - 12, 13))
            pygame.draw.ellipse(self.screen, (72, 185, 215), (screen_x + 8, screen_y + 10, CELL_SIZE - 16, 9), 2)
            pygame.draw.line(self.screen, (185, 235, 245), (screen_x + 11, screen_y + 12), (screen_x + CELL_SIZE - 12, screen_y + 12), 1)
        if x in (x1, x2) and y in (y1 + 1, y2 - 1):
            pygame.draw.rect(self.screen, (214, 116, 28), (screen_x + 3, screen_y + 5, 4, CELL_SIZE - 10))
        if y == y2 and x in (x1 + 1, x2 - 1):
            pygame.draw.circle(self.screen, (255, 190, 55), (screen_x + CELL_SIZE // 2, screen_y + 7), 2)
        return True

    def is_aurora_biome(self, biome):
        """Проверяет, нужно ли рисовать клетку металлическим стилем Авроры."""
        if biome is None:
            return False
        if getattr(biome, 'aurora_style', False):
            return True
        name = biome.name
        return 'Аврор' in name or 'Двигательный отсек' in name

    def draw_aurora_cell(self, x, y, screen_x, screen_y, biome):
        """Reuse identical pre-rendered metal tiles, rather than redrawing primitives."""
        cache = getattr(self, '_aurora_tile_cache', None)
        if cache is None:
            cache = self._aurora_tile_cache = {}
        # Some details depend on cell coordinates; repeat them by a fixed period.
        key = (id(biome), x % 187, y % 187, x % 11, y % 13)
        tile = cache.get(key)
        if tile is None:
            tile = pygame.Surface((CELL_SIZE, CELL_SIZE)).convert()
            original_screen = self.screen
            try:
                self.screen = tile
                self._paint_aurora_cell(x, y, 0, 0, biome)
            finally:
                self.screen = original_screen
            if len(cache) >= 2048:
                cache.clear()
            cache[key] = tile
        self.screen.blit(tile, (screen_x, screen_y))

    def _paint_aurora_cell(self, x, y, screen_x, screen_y, biome):
        """Рисует клетку Авроры в стиле стартовой капсулы:
        непроходимые клетки — как наружный корпус,
        проходимые — как внутренний металлический настил."""
        cell_rect = pygame.Rect(screen_x, screen_y, CELL_SIZE, CELL_SIZE)
        inner_rect = cell_rect.inflate(-4, -4)
        if not biome.passable:
            pygame.draw.rect(self.screen, (126, 139, 143), cell_rect)
            pygame.draw.rect(self.screen, (74, 86, 90), inner_rect)
            pygame.draw.rect(self.screen, (177, 188, 190), cell_rect, 2)
            pygame.draw.rect(self.screen, (36, 47, 52), inner_rect, 1)
            pygame.draw.line(self.screen, (105, 118, 122), (screen_x + 4, screen_y + CELL_SIZE // 2), (screen_x + CELL_SIZE - 4, screen_y + CELL_SIZE // 2), 1)
            pygame.draw.circle(self.screen, (210, 218, 220), (screen_x + 5, screen_y + 5), 1)
            pygame.draw.circle(self.screen, (210, 218, 220), (screen_x + CELL_SIZE - 6, screen_y + CELL_SIZE - 6), 1)
            if (x * 7 + y * 3) % 11 == 0:
                pygame.draw.ellipse(self.screen, (18, 45, 62), (screen_x + 6, screen_y + 8, CELL_SIZE - 12, 13))
                pygame.draw.ellipse(self.screen, (72, 185, 215), (screen_x + 8, screen_y + 10, CELL_SIZE - 16, 9), 2)
                pygame.draw.line(self.screen, (185, 235, 245), (screen_x + 11, screen_y + 12), (screen_x + CELL_SIZE - 12, screen_y + 12), 1)
            if (x + y) % 13 == 0:
                pygame.draw.circle(self.screen, (255, 190, 55), (screen_x + CELL_SIZE // 2, screen_y + 6), 2)
            if (x * 3 + y * 5) % 17 == 0:
                pygame.draw.rect(self.screen, (214, 116, 28), (screen_x + 3, screen_y + 5, 4, CELL_SIZE - 10))
            return
        # Цвет и узор зависят от конкретного отсека, а не от всей Авроры.
        name = biome.name
        base = tuple(int(c) for c in biome.color[:3])
        if getattr(biome, 'location_kind', '') == 'aurora_wreck':
            base = (91, 104, 113)
        token = sum((i + 1) * ord(c) for i, c in enumerate(name))
        def shade(mul, offset=0):
            return tuple(max(0, min(255, int(c * mul + offset))) for c in base)
        pygame.draw.rect(self.screen, shade(.43), cell_rect)
        pygame.draw.rect(self.screen, shade(.71), inner_rect)
        pygame.draw.rect(self.screen, shade(1.15), inner_rect, 1)
        pattern = token % 6
        if pattern == 0:  # продольные рёбра
            for delta in (9, 15, 21):
                pygame.draw.line(self.screen, shade(.48),
                                 (screen_x + delta, screen_y + 4),
                                 (screen_x + delta, screen_y + CELL_SIZE - 4), 2)
        elif pattern == 1:  # диагональная штриховка
            for delta in (0, 10, 20):
                pygame.draw.line(self.screen, shade(.48),
                                 (screen_x + delta + 3, screen_y + 4),
                                 (screen_x + min(delta + 12, CELL_SIZE - 4), screen_y + 20), 1)
        elif pattern == 2:  # две прямоугольные панели
            pygame.draw.rect(self.screen, shade(.52),
                             (screen_x + 5, screen_y + 5, CELL_SIZE - 10, 8), 1)
            pygame.draw.rect(self.screen, shade(.54),
                             (screen_x + 5, screen_y + 17, CELL_SIZE - 10, 8), 1)
        elif pattern == 3:  # решётчатый настил
            for delta in (8, 15, 22):
                pygame.draw.line(self.screen, shade(.55),
                                 (screen_x + 4, screen_y + delta),
                                 (screen_x + CELL_SIZE - 4, screen_y + delta), 1)
                pygame.draw.line(self.screen, shade(.55),
                                 (screen_x + delta, screen_y + 4),
                                 (screen_x + delta, screen_y + CELL_SIZE - 4), 1)
        elif pattern == 4:  # круглые крепления
            pygame.draw.circle(self.screen, shade(.54),
                               (screen_x + 15, screen_y + 15), 8, 2)
            pygame.draw.circle(self.screen, shade(1.25),
                               (screen_x + 15, screen_y + 15), 2)
        else:  # технические пластины
            pygame.draw.line(self.screen, shade(.45),
                             (screen_x + 4, screen_y + 15),
                             (screen_x + CELL_SIZE - 4, screen_y + 15), 2)
            pygame.draw.rect(self.screen, shade(.54),
                             (screen_x + 7, screen_y + 6, 16, 17), 1)
        for ox, oy in ((5, 5), (CELL_SIZE - 6, 5), (5, CELL_SIZE - 6),
                       (CELL_SIZE - 6, CELL_SIZE - 6)):
            pygame.draw.circle(self.screen, shade(1.35),
                               (screen_x + ox, screen_y + oy), 1)
        if (x * 3 + y * 7 + token) % 17 == 0:
            pygame.draw.line(self.screen, (88, 194, 202),
                             (screen_x + 9, screen_y + CELL_SIZE - 5),
                             (screen_x + CELL_SIZE - 9, screen_y + CELL_SIZE - 5), 2)

    def toggle_inventory(self):
        if self.state == 'playing':
            self.state = 'inventory'
        elif self.state == 'inventory':
            self.state = 'playing'
            self.notification = ''
        else:
            self.state = 'playing'
            self.selected_biome = None

    def toggle_crafting(self):
        if self.state == 'crafting':
            self.state = 'playing'
            self.notification = ''
            return
        if not self.can_craft_here():
            self.notification = 'Крафт доступен только в капсуле или на построенном полу'
            self.notification_timer = 120
            return
        self.state = 'crafting'
        self.craft_page = 0
        self.craft_station_index = 0
        self.selected_biome = None

    def toggle_map(self):
        if self.state == 'playing':
            self.state = 'map'
            self.map_center_x = self.player_x
            self.map_center_y = self.player_y
            self.selected_biome = None
        elif self.state == 'map':
            self.state = 'playing'
            self.selected_biome = None

    def toggle_pet_list(self):
        if self.state == 'pets':
            self.state = 'playing'
            self.notification = ''
            return
        self.state = 'pets'
        self.pet_list_page = 0
        self.selected_pet = None
        self.selected_biome = None

    def select_pet_from_list(self, number):
        """Клавиши 1–9 выбирают питомца именно на текущей странице."""
        pets = self._get_all_companions()
        index = self.pet_list_page * self.pets_per_page + number - 1
        if 1 <= number <= 9 and 0 <= index < len(pets):
            self.selected_pet = pets[index]
        else:
            self.selected_pet = None
            self.notification = 'На этой позиции нет питомца'
            self.notification_timer = 70

    def _get_selected_pet(self):
        pet = self.selected_pet
        if pet is not None and pet in self._get_all_companions() and (pet.hp > 0):
            return pet
        self.selected_pet = None
        self.notification = 'Сначала выберите питомца клавишей 1–9'
        self.notification_timer = 80
        return None

    def return_selected_pet_to_inventory(self):
        """E: отозвать выбранного спутника с поверхности или из пещеры."""
        pet = self._get_selected_pet()
        if pet is None:
            return
        free_slot = next((i for i, item in enumerate(self.inventory) if item is None), None)
        if free_slot is None or not self.add_to_inventory(pet.type):
            self.notification = 'Инвентарь заполнен — забрать питомца нельзя'
            self.notification_timer = 110
            return
        self._stored_companion_hp[free_slot] = (pet.type, pet.hp)
        self._clear_companion_target(pet)
        for animal in list(self.world.animals) + list(self.cave_animals):
            if getattr(animal, 'pet_combat_target', None) is pet:
                animal.pet_combat_target = None
        if pet in self.cave_animals:
            self.cave_animals.remove(pet)
        else:
            self.world.remove_animal(pet)
        self.selected_pet = None
        self.pet_list_page = min(self.pet_list_page, max(0, (len(self._get_all_companions()) - 1) // self.pets_per_page))
        self.notification = f'{pet.type.name} возвращён в инвентарь (HP {int(pet.hp)})'
        self.notification_timer = 110

    def heal_selected_pet(self):
        """U: одна аптечка восстанавливает до 35 HP выбранному питомцу."""
        pet = self._get_selected_pet()
        if pet is None:
            return
        if pet.hp >= pet.max_hp:
            self.notification = f'У {pet.type.name} уже полное здоровье'
            self.notification_timer = 80
            return
        if self.inventory_counts.get(first_aid_kit, 0) <= 0:
            self.notification = 'Для лечения питомца нужна аптечка'
            self.notification_timer = 100
            return
        if not self._remove_inventory_items_of_type(first_aid_kit, 1):
            self.notification = 'Не удалось использовать аптечку'
            self.notification_timer = 90
            return
        restored = min(35, pet.max_hp - pet.hp)
        pet.hp += restored
        self.notification = f'{pet.type.name}: +{int(restored)} HP ({int(pet.hp)}/{int(pet.max_hp)}), потрачена 1 аптечка'
        self.notification_timer = 110

    def map_screen_to_world(self, screen_x, screen_y):
        scale = self.map_base_scale * self.map_zoom
        center_x = SCREEN_SIZE / 2
        center_y = SCREEN_SIZE / 2
        world_x = (screen_x - center_x) / scale + self.map_center_x
        world_y = (screen_y - center_y) / scale + self.map_center_y
        return (world_x, world_y)

    def map_select_biome(self, screen_x, screen_y):
        self.map_panel_scroll = 0
        self._map_biome_species_cache = None
        world_x, world_y = self.map_screen_to_world(screen_x, screen_y)
        self.selected_biome = None
        if self.in_cave:
            for tunnel in reversed(self.cave_tunnels):
                if tunnel.contains(world_x, world_y):
                    self.selected_biome = tunnel
                    self.notification = f'Выбрано: {tunnel.name}'
                    self.notification_timer = 60
                    return
        biome = self.world.get_biome_at(world_x, world_y)
        if biome is not None:
            self.selected_biome = biome
            self.notification = f'Выбран биом: {biome.name}'
            self.notification_timer = 60
            return
        self.notification = 'Космическая пустота'
        self.notification_timer = 30

    def update_map_pan(self):
        keys = pygame.key.get_pressed()
        pan = 12 / self.map_zoom
        if keys[pygame.K_LEFT]:
            self.map_center_x -= pan
        if keys[pygame.K_RIGHT]:
            self.map_center_x += pan
        if keys[pygame.K_UP]:
            self.map_center_y -= pan
        if keys[pygame.K_DOWN]:
            self.map_center_y += pan
        self.map_center_x = max(0, min(MAP_SIZE, self.map_center_x))
        self.map_center_y = max(0, min(MAP_SIZE, self.map_center_y))

    def biome_screen_polygon(self, biome, scale, center_x, center_y):
        points = []
        for world_x, world_y in biome.map_points():
            screen_x = center_x + (world_x - self.map_center_x) * scale
            screen_y = center_y + (world_y - self.map_center_y) * scale
            points.append((int(screen_x), int(screen_y)))
        return points

    def draw_map_biome_label(self, biome, scale, center_x, center_y):
        if not biome.map_label or self.map_zoom < 0.55:
            return
        wx, wy = biome.center()
        sx = int(center_x + (wx - self.map_center_x) * scale)
        sy = int(center_y + (wy - self.map_center_y) * scale)
        lines = biome.map_label.split('\n')
        line_height = 16
        start_y = sy - len(lines) * line_height // 2
        for index, line in enumerate(lines):
            shadow = self.tiny_font.render(line, True, (0, 0, 0))
            label = self.tiny_font.render(line, True, (245, 245, 245))
            rect = label.get_rect(center=(sx, start_y + index * line_height))
            shadow_rect = rect.copy()
            shadow_rect.x += 2
            shadow_rect.y += 2
            self.screen.blit(shadow, shadow_rect)
            self.screen.blit(label, rect)

    def draw_caves_on_world_map(self, scale, center_x, center_y):
        overlay = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA)
        base_alpha = 115 if self.in_cave else 65
        current_tunnel = None
        if self.in_cave:
            current_tunnel = self.get_cave_tunnel_at(self.player_x, self.player_y)
        for tunnel in self.cave_tunnels:
            points = self.biome_screen_polygon(tunnel, scale, center_x, center_y)
            if len(points) < 3:
                continue
            if tunnel is current_tunnel:
                fill = (40, 245, 255, 165)
                border = (210, 255, 255, 220)
            else:
                fill = (20, 205, 220, base_alpha)
                border = (120, 245, 250, min(190, base_alpha + 60))
            pygame.draw.polygon(overlay, fill, points)
            pygame.draw.polygon(overlay, border, points, 1)
        self.screen.blit(overlay, (0, 0))
        for entrance in self.cave_entrances:
            x1, y1, _, _ = entrance['surface_rect']
            sx = int(center_x + (x1 - self.map_center_x) * scale)
            sy = int(center_y + (y1 - self.map_center_y) * scale)
            radius = max(3, int(5 * self.map_zoom))
            radius = min(radius, 8)
            pygame.draw.circle(self.screen, (15, 40, 45), (sx, sy), radius + 2)
            pygame.draw.circle(self.screen, (90, 245, 255), (sx, sy), radius, 2)

    def _draw_land_ecology_on_map(self, scale, center_x, center_y):
        """Обозначает наземные живые объекты при приближении карты.

        Не прорисовываем тысячи деревьев: равномерная выборка и обрезка
        по экрану ограничивают время отрисовки даже в больших лесах.
        """
        if self.in_cave or self.map_zoom < 0.9:
            return

        def draw_point(x, y, color, radius):
            px = int(center_x + (x - self.map_center_x) * scale)
            py = int(center_y + (y - self.map_center_y) * scale)
            if 6 <= px < SCREEN_SIZE - 6 and 6 <= py < SCREEN_SIZE - 6:
                pygame.draw.circle(self.screen, color, (px, py), radius)
        trees = self.world.tree_grid
        tree_step = max(1, len(trees) // 600)
        for index, (x, y) in enumerate(trees):
            if index % tree_step == 0:
                draw_point(x, y, (64, 170, 67), 2)
        land_plants = self.world.flora_grid
        plant_step = max(1, len(land_plants) // 750)
        for index, (x, y) in enumerate(land_plants):
            if index % plant_step == 0 and is_land_biome(self.world.get_biome_at(x, y)):
                draw_point(x, y, (177, 235, 90), 2)
        for animal in self.world.animals:
            if animal.hp > 0 and (not getattr(animal, 'is_companion', False)) and is_land_biome(self.world.get_biome_at(animal.x, animal.y)):
                draw_point(animal.x, animal.y, (255, 193, 82), 2)
        for moss in self.world.moss_colonies:
            draw_point(moss.x, moss.y, (230, 244, 112), 3)
        for golem in self.world.golem_colonies:
            if golem.phase in ('active', 'rubble', 'breeding'):
                col = GOLEM_DATA[golem.kind]['color']
                if golem.phase == 'rubble':
                    col = tuple((v // 2 for v in col))
                draw_point(golem.x, golem.y, col, 4 if golem.phase == 'active' else 2)

    def draw_map(self):
        self.screen.fill((4, 6, 9))
        scale = self.map_base_scale * self.map_zoom
        center_x = SCREEN_SIZE // 2
        center_y = SCREEN_SIZE // 2
        ordered_biomes = sorted(self.world.biomes, key=lambda biome: biome.priority)
        for biome in ordered_biomes:
            points = self.biome_screen_polygon(biome, scale, center_x, center_y)
            if len(points) < 3:
                continue
            xs = [p[0] for p in points]
            ys = [p[1] for p in points]
            if max(xs) < -30 or min(xs) > SCREEN_SIZE + 30 or max(ys) < -30 or (min(ys) > SCREEN_SIZE + 30):
                continue
            pygame.draw.polygon(self.screen, biome.color, points)
            if biome is self.selected_biome:
                border_color = (255, 235, 80)
                border_width = 3
            else:
                border_color = (max(0, biome.color[0] - 50), max(0, biome.color[1] - 50), max(0, biome.color[2] - 50))
                border_width = 1
            pygame.draw.polygon(self.screen, border_color, points, border_width)
        self.draw_caves_on_world_map(scale, center_x, center_y)
        # Plants and animals are listed only in the left biome information panel.
        for biome in self.world.biomes:
            if biome.cave_enabled:
                self.draw_map_biome_label(biome, scale, center_x, center_y)
        player_screen_x = int(center_x + (self.player_x - self.map_center_x) * scale)
        player_screen_y = int(center_y + (self.player_y - self.map_center_y) * scale)
        if -20 <= player_screen_x <= SCREEN_SIZE + 20 and -20 <= player_screen_y <= SCREEN_SIZE + 20:
            pygame.draw.circle(self.screen, (0, 90, 230), (player_screen_x, player_screen_y), 7)
            pygame.draw.circle(self.screen, (255, 255, 255), (player_screen_x, player_screen_y), 7, 2)
        map_title = 'КАРТА МИРА — ПЕЩЕРНЫЙ УРОВЕНЬ' if self.in_cave else 'АСТЕРОИД-КОЛЬЦО'
        title = self.large_font.render(map_title, True, (255, 255, 255))
        self.screen.blit(title, title.get_rect(center=(SCREEN_SIZE // 2, 26)))
        zoom_text = self.small_font.render(f'Зум: {self.map_zoom:.2f}x', True, (255, 255, 255))
        self.screen.blit(zoom_text, (10, 10))
        if self.selected_biome:
            self.draw_biome_info_panel()
        controls = self.tiny_font.render('M/ESC: закрыть | +/-: зум | стрелки: движение | клик: биом', True, (220, 220, 220))
        self.screen.blit(controls, controls.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE - 15)))

    def draw_cave_map(self):
        self.screen.fill((8, 8, 12))
        scale = self.map_base_scale * self.map_zoom
        center_x = SCREEN_SIZE // 2
        center_y = SCREEN_SIZE // 2
        for tunnel in self.cave_tunnels:
            x1, y1, x2, y2 = tunnel.rect
            sx1 = center_x + (x1 - self.map_center_x) * scale
            sy1 = center_y + (y1 - self.map_center_y) * scale
            sx2 = center_x + (x2 + 1 - self.map_center_x) * scale
            sy2 = center_y + (y2 + 1 - self.map_center_y) * scale
            rect = pygame.Rect(int(sx1), int(sy1), max(2, int(sx2 - sx1)), max(2, int(sy2 - sy1)))
            pygame.draw.rect(self.screen, tunnel.color, rect)
            border = (255, 230, 90) if tunnel is self.selected_biome else (85, 95, 110)
            pygame.draw.rect(self.screen, border, rect, 3 if tunnel is self.selected_biome else 1)
        for entrance in self.cave_entrances:
            x1, y1, x2, y2 = entrance['cave_rect']
            sx1 = center_x + (x1 - self.map_center_x) * scale
            sy1 = center_y + (y1 - self.map_center_y) * scale
            sx2 = center_x + (x2 + 1 - self.map_center_x) * scale
            sy2 = center_y + (y2 + 1 - self.map_center_y) * scale
            rect = pygame.Rect(int(sx1), int(sy1), max(4, int(sx2 - sx1)), max(4, int(sy2 - sy1)))
            pygame.draw.rect(self.screen, (20, 80, 110), rect)
            pygame.draw.rect(self.screen, (180, 235, 255), rect, 2)
        player_x = center_x + (self.player_x - self.map_center_x) * scale
        player_y = center_y + (self.player_y - self.map_center_y) * scale
        pygame.draw.circle(self.screen, (0, 120, 230), (int(player_x), int(player_y)), 8)
        pygame.draw.circle(self.screen, (255, 255, 255), (int(player_x), int(player_y)), 8, 2)
        title = self.large_font.render('КАРТА ПЕЩЕР', True, (255, 255, 255))
        self.screen.blit(title, title.get_rect(center=(SCREEN_SIZE // 2, 26)))
        zoom_text = self.small_font.render(f'Зум: {self.map_zoom:.2f}x', True, (255, 255, 255))
        self.screen.blit(zoom_text, (10, 10))
        if self.selected_biome:
            self.draw_biome_info_panel()
        controls = self.tiny_font.render('M/ESC: закрыть | +/-: зум | стрелки: движение | клик: секция', True, (220, 220, 220))
        self.screen.blit(controls, controls.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE - 15)))


    def _map_real_biome_population(self, biome):
        """O(1) для ресурсов/растений; небольшой проход только по подвижной фауне.

        Индекс создаётся однажды при старте и автоматически поддерживает
        подбор, выпадение, строительство, респавн и уничтожение предметов.
        """
        from collections import Counter
        cave = self.in_cave and biome in self.cave_tunnels
        lookup = self.get_cave_tunnel_at if cave else self.world.get_biome_at
        if cave:
            resources = Counter(self.cave_object_grid.biome_counts.get(biome, ()))
            flora = Counter(self.cave_flora_grid.biome_counts.get(biome, ()))
            animals = self.cave_animals
        else:
            resources = Counter(self.world.object_grid.biome_counts.get(biome, ()))
            flora = Counter(self.world.flora_grid.biome_counts.get(biome, ()))
            flora.update(self.world.tree_grid.biome_counts.get(biome, ()))
            animals = self.world.animals
        fauna = Counter()
        x1, y1, x2, y2 = biome.rect

        def belongs(x, y):
            # Прямоугольный фильтр отсекает почти все вызовы геометрии.
            return (x1 <= x <= x2 and y1 <= y <= y2
                    and lookup(x, y) is biome)

        if not cave:
            for moss in self.world.moss_colonies:
                if belongs(moss.x, moss.y):
                    flora['Бешеный мох'] += 1
            for golem in self.world.golem_colonies:
                if belongs(golem.x, golem.y):
                    if golem.phase in ('active', 'rubble'):
                        fauna[GOLEM_DATA[golem.kind]['name']] += 1
                    elif golem.phase == 'breeding':
                        fauna['Големогенные бактерии (' + golem.kind + ')'] += 1
            for gx, gy in self.world.golem_dung_piles:
                if belongs(gx, gy):
                    resources['Навоз (субстрат големов)'] += 1
        for animal in animals:
            if animal.hp > 0 and not getattr(animal, 'is_companion', False):
                if belongs(animal.x, animal.y):
                    fauna[animal.type.name] += 1
        return (resources, flora, fauna)

    def _map_info_panel_area(self):
        return pygame.Rect(10, 70, 270, SCREEN_SIZE - 100)


    def draw_biome_info_panel(self):
        """Рисование панели за O(видимых строк); строки и текст кешируются."""
        biome = self.selected_biome
        if biome is None:
            return
        previous = self._map_biome_species_cache
        if previous is None or previous[0] is not biome:
            counts = self._map_real_biome_population(biome)
            self._map_biome_species_cache = (biome, counts)
            self._map_info_render_cache = None
            self.map_panel_scroll = 0
        else:
            counts = previous[1]
        area = self._map_info_panel_area()
        render_cache = getattr(self, '_map_info_render_cache', None)
        if render_cache is None or render_cache[0] is not biome:
            rows = self._build_biome_panel_lines(biome, counts, area.width - 24)
            # pygame.font.render вызывается только при выборе, не ежесекундно.
            drawn = []
            for message, color, bold in rows:
                font = self.small_font if bold else self.tiny_font
                drawn.append(font.render(message, True, color))
            render_cache = (biome, drawn)
            self._map_info_render_cache = render_cache
        drawn = render_cache[1]
        self.screen.fill((12, 21, 26), area)
        pygame.draw.rect(self.screen, (239, 212, 80), area, 2)
        px, py = area.x + 10, area.y
        caption = self.small_font.render(biome.name, True, (255, 245, 177))
        if caption.get_width() > area.width - 24:
            caption = self.tiny_font.render(biome.name, True, (255, 245, 177))
        # Длинные заголовки отсеков тоже обрезаются по границам панели.
        heading_clip = pygame.Rect(px, py + 6, area.width - 22, 25)
        old_clip = self.screen.get_clip()
        self.screen.set_clip(heading_clip)
        self.screen.blit(caption, (px, py + 8))
        self.screen.set_clip(old_clip)
        pygame.draw.line(self.screen, (93, 115, 107), (px, py + 34),
                         (area.right - 10, py + 34))
        row_h = 19
        visible_top, visible_bottom = py + 43, area.bottom - 13
        visible_h = visible_bottom - visible_top
        max_scroll = max(0, len(drawn) * row_h - visible_h)
        self.map_panel_scroll = max(0, min(self.map_panel_scroll, max_scroll))
        first = max(0, self.map_panel_scroll // row_h)
        last = min(len(drawn), (self.map_panel_scroll + visible_h) // row_h + 2)
        self.screen.set_clip(pygame.Rect(px, visible_top, area.width - 24, visible_h))
        try:
            for i in range(first, last):
                self.screen.blit(drawn[i], (px, visible_top + i * row_h - self.map_panel_scroll))
        finally:
            self.screen.set_clip(old_clip)
        if max_scroll:
            marker_h = max(18, round(visible_h * visible_h / (max_scroll + visible_h)))
            marker_y = visible_top + round((visible_h - marker_h) * self.map_panel_scroll / max_scroll)
            pygame.draw.rect(self.screen, (65, 79, 75), (area.right - 7, visible_top, 3, visible_h))
            pygame.draw.rect(self.screen, (174, 206, 170), (area.right - 7, marker_y, 3, marker_h))

    def get_camera_origin(self):
        """Исходное положение камеры (используется логикой игры)."""
        return (self.player_x - VIEW_RADIUS, self.player_y - VIEW_RADIUS)

    def get_render_camera_origin(self):
        """Камера отрисовки: радиус меняется с масштабом изображения."""
        return (self.player_x - self._render_view_radius, self.player_y - self._render_view_radius)

    def screen_to_world(self, mouse_x, mouse_y):
        """Точное наведение, взаимодействие и сканирование с учётом зума."""
        tile_pixels = CELL_SIZE * self.camera_zoom
        return (self.player_x + 0.5 + (mouse_x - SCREEN_SIZE / 2) / tile_pixels, self.player_y + 0.5 + (mouse_y - SCREEN_SIZE / 2) / tile_pixels)

    @staticmethod
    def _make_blue_edge_haze():
        """Голубая дымка: максимальна по краям, прозрачна в центре."""
        haze = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA)
        fade_width = int(SCREEN_SIZE * 0.39)
        for inset in range(fade_width):
            fraction = 1.0 - inset / max(1, fade_width - 1)
            alpha = round(195 * fraction ** 2.15)
            if alpha < 1:
                continue
            length = SCREEN_SIZE - 2 * inset
            if length <= 0:
                break
            pygame.draw.rect(haze, (50, 167, 224, alpha), pygame.Rect(inset, inset, length, length), 1)
        return haze.convert_alpha()

    def _draw_blue_edge_haze(self):
        if self.camera_zoom >= 1.0:
            return
        amount = (1.0 - self.camera_zoom) / (1.0 - CAMERA_ZOOM_MIN)
        self._blue_edge_haze.set_alpha(round(255 * max(0.0, min(1.0, amount))))
        self.screen.blit(self._blue_edge_haze, (0, 0))

    def _change_camera_zoom(self, wheel_steps):
        if not wheel_steps:
            return
        new_zoom = self.camera_zoom * CAMERA_ZOOM_STEP ** wheel_steps
        self.camera_zoom = max(CAMERA_ZOOM_MIN, min(CAMERA_ZOOM_MAX, new_zoom))
        self.mouse_world_x, self.mouse_world_y = self.screen_to_world(*pygame.mouse.get_pos())

    @staticmethod
    def _make_night_edge_vignette():
        """Непрозрачный чёрный край с мягким сходом к центру."""
        surface = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA)
        width = max(1, int(SCREEN_SIZE * NIGHT_EDGE_FADE))
        for inset in range(width):
            t = 1.0 - inset / max(1, width - 1)
            alpha = round(255 * t ** 2.0)
            if alpha > 0:
                pygame.draw.rect(surface, (0, 0, 0, alpha), (inset, inset, SCREEN_SIZE - 2 * inset, SCREEN_SIZE - 2 * inset), 1)
        return surface.convert_alpha()

    def _draw_night_edge_vignette(self):
        if not NIGHT_EDGE_VIGNETTE or not getattr(self, 'setting_water_darkening_enabled', True):
            return
        brightness = self._solar_brightness()
        if self.in_cave:
            strength = 0.76
        else:
            strength = max(0.0, min(1.0, (0.74 - brightness) / 0.74))
            strength = strength ** 1.55
        if strength <= 0.002:
            return
        self._night_edge_vignette.set_alpha(round(strength * 255))
        self.screen.blit(self._night_edge_vignette, (0, 0))

    def _hd_source(self, entity):
        """Не увеличиваем пиксели: создаём фигуры заново из исходных данных.

        Первый запрос для вида выполняется один раз, затем результат
        переиспользуется всеми экземплярами данного вида.
        """
        key = id(entity)
        if key in self._hd_source_cache:
            return self._hd_source_cache[key]
        base = getattr(entity, 'image', None)
        if base is None:
            self._hd_source_cache[key] = None
            return None
        factor = ZOOM_DETAIL_SOURCE_FACTOR
        source = None
        try:
            if isinstance(entity, FloraType):
                source = generate_subnautica_flora_sprite(entity.kind, round(entity.size * factor), seed=entity.seed)
            elif isinstance(entity, AnimalType):
                if getattr(entity, 'fastener_hd_image', None) is not None:
                    source = entity.fastener_hd_image
                elif entity.sprite_kind is not None:
                    source = generate_subnautica_creature_sprite(entity.sprite_kind, max(12, round(entity.fish_length * factor)), max(6, round(entity.fish_width * factor)))
                elif getattr(entity, '_hd_fish_palette', None) is not None:
                    state = random.getstate()
                    try:
                        random.seed(_stable_preview_seed(entity.name))
                        source = generate_pike_style_sprite(max(12, round(entity.fish_length * factor)), max(6, round(entity.fish_width * factor)), *entity._hd_fish_palette)
                    finally:
                        random.setstate(state)
            elif isinstance(entity, ObjectType):
                high_size = max(100, round(entity.size * factor * 1.4))
                if getattr(entity, 'is_buildable', False):
                    source = generate_building_sprite(entity.name, high_size, entity.color, wall=getattr(entity, 'is_wall', False))
                elif entity.name in _HIGH_RES_RESOURCE_NAMES:
                    source = generate_highres_resource_preview(entity, high_size)
                else:
                    source = generate_tech_item_sprite(entity.name, high_size, entity.color, is_weapon=getattr(entity, 'is_weapon', False), weapon_type=getattr(entity, 'weapon_type', ''))
        except (ValueError, TypeError, pygame.error):
            source = None
        if source is None:
            source = pygame.transform.smoothscale(base, (max(2, round(base.get_width() * factor)), max(2, round(base.get_height() * factor))))
        self._hd_source_cache[key] = source
        return source

    def _hd_image(self, entity, zoom, angle=None, glow=False):
        """Кэшированный экранный LOD без изменения игровых размеров объекта."""
        base = entity.glow_image if glow else entity.image
        if base is None:
            return None
        bucket = round(zoom / 0.04) * 0.04
        rotation = round(angle / ZOOM_DETAIL_ROTATION_STEP) * ZOOM_DETAIL_ROTATION_STEP % 360 if angle is not None else None
        key = (id(entity), bucket, rotation, glow)
        result = self._hd_sprite_cache.get(key)
        if result is not None:
            self._hd_sprite_cache.move_to_end(key)
            return result
        src = self._hd_source(entity)
        if src is None:
            return None
        if glow:
            src = self._hd_glow_source_cache.get(id(entity))
            if src is None:
                src = generate_bioluminescence_sprite(self._hd_source(entity), entity.kind if isinstance(entity, FloraType) else entity.sprite_kind or entity.name, is_plant=isinstance(entity, FloraType), seed=getattr(entity, 'seed', 0))
                self._hd_glow_source_cache[id(entity)] = src
            if src is None:
                return None
        sz = (max(1, round(base.get_width() * bucket)), max(1, round(base.get_height() * bucket)))
        result = pygame.transform.smoothscale(src, sz)
        if rotation is not None:
            result = pygame.transform.rotate(result, rotation)
        self._hd_sprite_cache[key] = result
        if len(self._hd_sprite_cache) > ZOOM_DETAIL_ROTATED_CACHE_MAX:
            self._hd_sprite_cache.popitem(last=False)
        return result

    def _hd_micro_tile(self, x, y, tile_px, cave=False, biome=None):
        """Рисует НОВЫЕ мини-детали дна на итоговой сетке пикселей."""
        variant = self._stable_visual_hash('zoom_details', x, y) % 48
        theme_id = 'cave' if cave else biome.name if biome else 'none'
        key = (theme_id, variant, tile_px)
        cached = self._hd_floor_cache.get(key)
        if cached is not None:
            self._hd_floor_cache.move_to_end(key)
            return cached
        seed = self._stable_visual_hash('zoom_micro', theme_id, variant)
        rng = random.Random(seed)
        surf = pygame.Surface((tile_px, tile_px), pygame.SRCALPHA)
        scale = tile_px / CELL_SIZE
        if cave:
            palette = ((86, 100, 120), (106, 112, 132), (43, 47, 60), (162, 174, 191))
        else:
            theme = self._surface_theme_for_biome(biome)
            if not theme:
                return None
            palette = tuple(theme['pebble_palette']) + (theme['highlight'], theme['shadow'])
        for i in range(22):
            px = rng.randint(1, tile_px - 2)
            py = rng.randint(1, tile_px - 2)
            color = palette[rng.randrange(len(palette))]
            radius = rng.choice((1, 1, 1, max(1, round(scale))))
            pygame.draw.circle(surf, (*color, rng.randint(75, 170)), (px, py), radius)
            if i % 5 == 0 and radius > 1:
                pygame.draw.circle(surf, (220, 232, 232, 120), (max(0, px - 1), max(0, py - 1)), 1)
        for j in range(rng.randint(2, 4)):
            cx = rng.randint(3, tile_px - 4)
            cy = rng.randint(3, tile_px - 4)
            delta = max(3, round(4 * scale))
            pts = [(cx, cy), (cx + rng.randint(-delta, delta), cy + rng.randint(-delta, delta)), (cx + rng.randint(-delta * 2, delta * 2), cy + rng.randint(-delta * 2, delta * 2))]
            pygame.draw.lines(surf, (*palette[j % len(palette)], 115), False, pts, 1)
        if scale >= 1.4 and (not cave) and (not is_land_biome(biome)) and (rng.random() < 0.28):
            px = rng.randint(tile_px // 4, tile_px * 3 // 4)
            py = rng.randint(tile_px // 4, tile_px * 3 // 4)
            radius = max(3, round(2.2 * scale))
            color = (205, 136, 109, 180)
            for i in range(5):
                a = i * math.tau / 5
                xx = round(px + math.cos(a) * radius)
                yy = round(py + math.sin(a) * radius)
                pygame.draw.line(surf, color, (px, py), (xx, yy), max(1, round(0.8 * scale)))
            pygame.draw.circle(surf, (242, 189, 133, 210), (px, py), max(1, round(scale)))
        self._hd_floor_cache[key] = surf
        if len(self._hd_floor_cache) > 550:
            self._hd_floor_cache.popitem(last=False)
        return surf

    def _draw_zoom_details(self, zoom, canvas_px, screen_offset):
        """Второй проход В ИСТИННОМ разрешении экрана, под ночной маской."""
        if not ZOOM_DETAIL_ENABLED or zoom < ZOOM_DETAIL_START:
            return
        tile_px = CELL_SIZE * zoom
        tile_int = max(4, round(tile_px))
        layer = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA)
        half = SCREEN_SIZE * 0.5

        def position(x, y):
            return (round(half + (x - self.player_x) * tile_px), round(half + (y - self.player_y) * tile_px))

        def sprite(entity, center, angle=None, fit=None):
            image = self._hd_image(entity, zoom, angle)
            if image is None:
                return
            if fit is not None:
                image = self._fit_surface(image, *fit)
            layer.blit(image, image.get_rect(center=center))
        margin = SCREEN_SIZE / (2 * tile_px) + 2
        left = max(0, math.floor(self.player_x - margin))
        right = min(MAP_SIZE - 1, math.ceil(self.player_x + margin))
        top = max(0, math.floor(self.player_y - margin))
        bottom = min(MAP_SIZE - 1, math.ceil(self.player_y + margin))
        objects = self._get_current_object_grid()
        plants = self._current_flora_grid()
        dropped = self._current_dropped_items()
        planter_types = self._planter_types()
        for y in range(top, bottom + 1):
            for x in range(left, right + 1):
                at = position(x, y)
                if not (-tile_px * 2 < at[0] < SCREEN_SIZE + tile_px * 2 and -tile_px * 2 < at[1] < SCREEN_SIZE + tile_px * 2):
                    continue
                cave_tunnel = self.get_cave_tunnel_at(x, y) if self.in_cave else None
                biome = None if self.in_cave else self.world.get_biome_at(x, y)
                if self.in_cave:
                    can_detail = cave_tunnel is not None
                else:
                    can_detail = biome is not None and (not self.is_aurora_biome(biome)) and (not (self.start_capsule_rect[0] <= x <= self.start_capsule_rect[2] and self.start_capsule_rect[1] <= y <= self.start_capsule_rect[3]))
                if can_detail:
                    micro = self._hd_micro_tile(x, y, tile_int, self.in_cave, biome)
                    if micro is not None:
                        layer.blit(micro, micro.get_rect(center=at))
        for y in range(top, bottom + 1):
            for x in range(left, right + 1):
                at = position(x, y)
                if not (-130 < at[0] < SCREEN_SIZE + 130 and -130 < at[1] < SCREEN_SIZE + 130):
                    continue
                flora = plants.get((x, y))
                if flora is not None:
                    sprite(flora, at)
                obj = objects.get((x, y))
                if obj is not None and (not getattr(obj, 'is_buildable', False)):
                    sprite(obj, at)
                contents = dropped.get((x, y))
                if contents:
                    item = contents[-1]
                    sprite(item, at)
        for animal in self.get_nearby_animals(margin + 2):
            if animal.hp <= 0 or animal.type.image is None:
                continue
            at = position(animal.x, animal.y)
            if -400 < at[0] < SCREEN_SIZE + 400 and -400 < at[1] < SCREEN_SIZE + 400:
                if getattr(animal, 'fastener_anchored', False):
                    head = getattr(animal.type, 'fastener_anchor_hd_image', None)
                    if head is not None:
                        size = max(6, round(44 * zoom))
                        head = pygame.transform.smoothscale(head, (size, size))
                        layer.blit(head, head.get_rect(center=at))
                else:
                    sprite(animal.type, at, getattr(animal, 'visual_angle', 0))
        if not self.in_cave:
            self._draw_mad_moss(0, 0, surface=layer, zoom=zoom)
        for y in range(top, bottom + 1):
            for x in range(left, right + 1):
                obj = objects.get((x, y))
                if obj is None or not getattr(obj, 'is_buildable', False):
                    continue
                at = position(x, y)
                sprite(obj, at)
                if obj in planter_types:
                    planted = self.planted_flora.get(self._planter_key(x, y), ())
                    offsets = ((0, 1), (-8, -3), (8, -3), (-8, 7), (8, 7), (0, -8), (0, 9))
                    max_size = 20 if len(planted) <= 2 else 15
                    for i, plant in enumerate(planted):
                        ox, oy = offsets[i % len(offsets)]
                        sprite(plant, (round(at[0] + ox * zoom), round(at[1] + oy * zoom)), fit=(round(max_size * zoom), round(max_size * zoom)))
        center = (round(half), round(half))

        def blit_zoomed(image, at):
            if image is None:
                return
            size = (max(1, round(image.get_width() * zoom)), max(1, round(image.get_height() * zoom)))
            enlarged = pygame.transform.smoothscale(image, size)
            layer.blit(enlarged, enlarged.get_rect(center=at))
        if self.active_vehicle is not None:
            blit_zoomed(self.get_vehicle_rotated(self.active_vehicle, self.player_angle), center)
        else:
            if self.equipped_weapon is not None:
                radians = math.radians(self.player_angle)
                weapon_pos = (round(half + math.cos(radians) * tile_px * 0.55), round(half + math.sin(radians) * tile_px * 0.55))
                blit_zoomed(self.get_weapon_rotated(self.equipped_weapon, self.player_angle), weapon_pos)
            blit_zoomed(self.get_diver_rotated(self.player_angle), center)
        if self._night_scene_alpha() > 0:
            mask = pygame.transform.smoothscale(self.lighting_overlay, (canvas_px, canvas_px))
            layer.blit(mask, (screen_offset, screen_offset), special_flags=pygame.BLEND_RGBA_SUB)
        self.screen.blit(layer, (0, 0))
        if BIOLUMINESCENCE_ENABLED:
            night = 1.0 if self.in_cave else max(0.0, 1.0 - self._solar_brightness() * 1.35)
            alpha = round((BIOGLOW_DAY_ALPHA + (BIOGLOW_NIGHT_ALPHA - BIOGLOW_DAY_ALPHA) * night) * (0.88 + 0.12 * math.sin(self.frame_count * 0.045)))
            for y in range(top, bottom + 1):
                for x in range(left, right + 1):
                    at = position(x, y)
                    flora = plants.get((x, y))
                    if flora is None or flora.glow_image is None:
                        continue
                    image = self._hd_image(flora, zoom, glow=True)
                    if image is not None:
                        image.set_alpha(alpha)
                        self.screen.blit(image, image.get_rect(center=at))
                        image.set_alpha(None)
            for animal in self.get_nearby_animals(margin + 2):
                if animal.hp <= 0 or animal.type.glow_image is None:
                    continue
                at = position(animal.x, animal.y)
                image = self._hd_image(animal.type, zoom, getattr(animal, 'visual_angle', 0), glow=True)
                if image is not None:
                    image.set_alpha(alpha)
                    self.screen.blit(image, image.get_rect(center=at))
                    image.set_alpha(None)

    def _night_scene_alpha(self):
        if not getattr(self, 'setting_water_darkening_enabled', True):
            return 0
        if self.in_cave:
            return 250
        brightness = self._solar_brightness()
        if MIDNIGHT_BLACKOUT and min(self._visual_day_time(), 1.0 - self._visual_day_time()) <= MIDNIGHT_BLACKOUT_HALF_WINDOW:
            return 255
        twilight = max(0.0, min(1.0, (0.72 - brightness) / 0.72))
        return round(255 * twilight ** 1.5)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.MOUSEMOTION:
                mouse_x, mouse_y = event.pos
                self.mouse_world_x, self.mouse_world_y = self.screen_to_world(mouse_x, mouse_y)
            elif event.type == pygame.MOUSEWHEEL:
                if self.state == 'map':
                    over_panel = self.selected_biome is not None and self._map_info_panel_area().collidepoint(pygame.mouse.get_pos())
                    if over_panel:
                        self.map_panel_scroll = max(0, self.map_panel_scroll - event.y * 57)
                    elif event.y > 0:
                        self.map_zoom = min(self.map_max_zoom, self.map_zoom * 1.15)
                    elif event.y < 0:
                        self.map_zoom = max(self.map_min_zoom, self.map_zoom / 1.15)
                elif self.state == 'scanner':
                    self.scanner_scroll_offset = max(0, min(self.scanner_scroll_max, self.scanner_scroll_offset - event.y * 50))
                elif self.state == 'inventory':
                    pages = max(1, (len(self.whale_quests) + 1) // 2)
                    self.whale_quest_panel_page = max(0, min(pages - 1, self.whale_quest_panel_page - event.y))
                elif self.state == 'playing':
                    self._change_camera_zoom(event.y)
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_n:
                    if self.state == 'playing' or self.settings_open:
                        self.settings_open = not self.settings_open
                elif event.key == pygame.K_ESCAPE:
                    if self.settings_open:
                        self.settings_open = False
                    elif self.state != 'playing':
                        self.state = 'playing'
                        self.notification = ''
                        self.selected_biome = None
                    else:
                        self.running = False
                elif event.key == pygame.K_o and self.state in ('playing', 'crew_panel'):
                    # При одиночном прохождении панель бригады недоступна.
                    if self.state == 'crew_panel':
                        self.state = 'playing'
                    elif self.setting_workers_enabled and not self.settings_open:
                        self.state = 'crew_panel'
                elif self.state=='crew_panel' and event.key in (pygame.K_1,pygame.K_2,pygame.K_3,pygame.K_4,pygame.K_5):
                    self.foreman.order={pygame.K_1:'авто',pygame.K_2:'добыча',
                                        pygame.K_3:'строительство',pygame.K_4:'производство',
                                        pygame.K_5:'охота'}[event.key]
                    for w in self.foreman.workers:w.rethink=1000
                elif event.key == pygame.K_F8:
                    self.force_midnight_preview = not self.force_midnight_preview
                    self.notification = 'Предпросмотр полной ночи: ВКЛ (F8)' if self.force_midnight_preview else 'Предпросмотр полной ночи: ВЫКЛ (F8)'
                    self.notification_timer = 110
                elif event.key == pygame.K_l and self.active_vehicle is not None and (self.state == 'playing'):
                    self.vehicle_headlights_on = not self.vehicle_headlights_on
                    self.notification = 'Фары подлодки включены' if self.vehicle_headlights_on else 'Фары подлодки выключены'
                    self.notification_timer = 75
                elif event.key == pygame.K_r:
                    self.toggle_inventory()
                elif event.key == pygame.K_i:
                    self.toggle_crafting()
                elif event.key == pygame.K_m:
                    self.toggle_map()
                elif event.key == pygame.K_t:
                    self.toggle_pet_list()
                elif event.key == pygame.K_f and self.state in ('playing', 'inventory'):
                    self.handle_vehicle_action()
                elif event.key == pygame.K_c and self.state == 'playing':
                    self.use_cave_entrance()
                elif event.key == pygame.K_b and self.state == 'playing':
                    self.build()
                elif event.key == pygame.K_x and self.state == 'playing':
                    self.destroy_build()
                elif event.key == pygame.K_e and self.state == 'playing':
                    self.pick_up()
                elif event.key == pygame.K_SPACE and self.state == 'playing':
                    self.attack()
                elif event.key == pygame.K_u and self.state in ('playing', 'inventory'):
                    self.use_item()
                elif event.key == pygame.K_q and self.state == 'playing':
                    self.equip_weapon()
                elif event.key == pygame.K_p and self.state == 'inventory':
                    self.drop_selected_item()
                elif event.key == pygame.K_h and self.state == 'inventory':
                    self.spawn_selected_companion()
                if self.state == 'inventory':
                    pages = max(1, (len(self.whale_quests) + 1) // 2)
                    if event.key == pygame.K_PAGEDOWN:
                        self.whale_quest_panel_page = min(pages - 1, self.whale_quest_panel_page + 1)
                    elif event.key == pygame.K_PAGEUP:
                        self.whale_quest_panel_page = max(0, self.whale_quest_panel_page - 1)
                if self.state == 'pets':
                    pets = self._get_all_companions()
                    max_page = max(0, (len(pets) - 1) // self.pets_per_page)
                    if event.key in (pygame.K_RIGHT, pygame.K_PAGEDOWN):
                        self.pet_list_page = min(max_page, self.pet_list_page + 1)
                        self.selected_pet = None
                    elif event.key in (pygame.K_LEFT, pygame.K_PAGEUP):
                        self.pet_list_page = max(0, self.pet_list_page - 1)
                        self.selected_pet = None
                    elif pygame.K_1 <= event.key <= pygame.K_9:
                        self.select_pet_from_list(event.key - pygame.K_0)
                    elif pygame.K_KP1 <= event.key <= pygame.K_KP9:
                        self.select_pet_from_list(event.key - pygame.K_KP1 + 1)
                    elif event.key == pygame.K_e:
                        self.return_selected_pet_to_inventory()
                    elif event.key == pygame.K_u:
                        self.heal_selected_pet()
                if self.state == 'planting':
                    plants = self._researched_plant_list()
                    max_page = max(0, (len(plants) - 1) // self.plants_per_page)
                    if event.key in (pygame.K_RIGHT, pygame.K_PAGEDOWN):
                        self.planting_page = min(max_page, self.planting_page + 1)
                    elif event.key in (pygame.K_LEFT, pygame.K_PAGEUP):
                        self.planting_page = max(0, self.planting_page - 1)
                    elif pygame.K_1 <= event.key <= pygame.K_9:
                        self.plant_researched_flora(event.key - pygame.K_1)
                if self.state == 'map':
                    if event.key in (pygame.K_EQUALS, pygame.K_PLUS, pygame.K_KP_PLUS, pygame.K_PAGEUP):
                        self.map_zoom = min(self.map_max_zoom, self.map_zoom * 1.2)
                    elif event.key in (pygame.K_MINUS, pygame.K_KP_MINUS, pygame.K_PAGEDOWN):
                        self.map_zoom = max(self.map_min_zoom, self.map_zoom / 1.2)
                if self.state == 'crafting':
                    if event.key == pygame.K_TAB:
                        stations = self.get_available_crafting_stations()
                        if stations:
                            self.craft_station_index = (self.craft_station_index + 1) % len(stations)
                            self.craft_page = 0
                    else:
                        station_recipes = self.get_current_station_recipes()
                        max_station_page = max(0, (len(station_recipes) - 1) // self.recipes_per_page)
                        if event.key == pygame.K_RIGHT:
                            if self.craft_page < max_station_page:
                                self.craft_page += 1
                        elif event.key == pygame.K_LEFT:
                            if self.craft_page > 0:
                                self.craft_page -= 1
                        elif pygame.K_1 <= event.key <= pygame.K_9:
                            index = event.key - pygame.K_1
                            recipe_index = self.craft_page * self.recipes_per_page + index
                            if recipe_index < len(station_recipes):
                                if not self.can_craft_here():
                                    self.state = 'playing'
                                    self.notification = 'Здесь нет доступной станции крафта'
                                    self.notification_timer = 120
                                    continue
                                recipe = station_recipes[recipe_index]
                                if recipe.can_craft(self.inventory_counts):
                                    free_slots = sum((1 for slot in self.inventory if slot is None))
                                    possible_slots = free_slots + sum(recipe.ingredients.values())
                                    if possible_slots < recipe.result_count:
                                        self.notification = 'Нет места в инвентаре!'
                                        self.notification_timer = 60
                                        continue
                                    result = recipe.craft(self.inventory, self.inventory_counts)
                                    if result is not None:
                                        crafted = 0
                                        for _ in range(recipe.result_count):
                                            if self.add_to_inventory(result):
                                                crafted += 1
                                        suffix = f' x{crafted}' if crafted > 1 else ''
                                        self.notification = f'Создано: {result.name}{suffix}'
                                        self.notification_timer = 60
                                else:
                                    self.notification = 'Не хватает ресурсов'
                                    self.notification_timer = 60
            elif event.type == pygame.MOUSEBUTTONDOWN and self.settings_open:
                if event.button == 1:
                    mouse_x, mouse_y = event.pos
                    option_rects = self._settings_option_rects()
                    if option_rects[0].collidepoint(mouse_x, mouse_y):
                        self.setting_plankton_enabled = not self.setting_plankton_enabled
                        if not self.setting_plankton_enabled:
                            self._night_sparks_visible = 0
                            self._night_sparks.clear()
                    elif option_rects[1].collidepoint(mouse_x, mouse_y):
                        self.setting_seabed_details_enabled = not self.setting_seabed_details_enabled
                    elif option_rects[2].collidepoint(mouse_x, mouse_y):
                        self.setting_water_darkening_enabled = not self.setting_water_darkening_enabled
                    elif option_rects[3].collidepoint(mouse_x, mouse_y):
                        self.setting_workers_enabled = not self.setting_workers_enabled
                        # Без пересоздания бригады: при обратном включении
                        # её инвентарь, здоровье, здания и запасы останутся прежними.
                        if not self.setting_workers_enabled and self.state == 'crew_panel':
                            self.state = 'playing'
                        self.notification = ('Одиночный режим: рабочие отключены'
                                             if not self.setting_workers_enabled
                                             else 'Рабочие снова включены')
                        self.notification_timer = 110
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                if self.state == 'playing':
                    if self.active_vehicle is not None:
                        self.fire_vehicle_torpedo(self.mouse_world_x, self.mouse_world_y)
                    else:
                        self.shoot(self.mouse_world_x, self.mouse_world_y)
                elif self.state == 'inventory':
                    mouse_x, mouse_y = event.pos
                    start_x, start_y, total_width, total_height = self._inventory_grid_layout()
                    if start_x <= mouse_x <= start_x + total_width and start_y <= mouse_y <= start_y + total_height:
                        col = (mouse_x - start_x) // (SLOT_SIZE + SLOT_MARGIN)
                        row = (mouse_y - start_y) // (SLOT_SIZE + SLOT_MARGIN)
                        if col < INVENTORY_COLS and row < INVENTORY_ROWS:
                            index = row * INVENTORY_COLS + col
                            if index < INVENTORY_SIZE:
                                self.active_slot_index = index
                                item = self.inventory[index]
                                if item:
                                    self.notification = f'Выбран: {item.name}'
                                else:
                                    self.notification = 'Слот пуст'
                                self.notification_timer = 60
                elif self.state == 'map':
                    mouse_x, mouse_y = event.pos
                    if not (self.selected_biome is not None and self._map_info_panel_area().collidepoint(mouse_x, mouse_y)):
                        self.map_select_biome(mouse_x, mouse_y)
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 3:
                if self.state == 'playing':
                    if self.active_vehicle is armored_carrier_type:
                        self._land_vehicle_shot(self.mouse_world_x, self.mouse_world_y, 'machine_gun')
                    elif self._is_scanner_selected():
                        self.scan_with_scanner()
                    else:
                        self.open_planting_menu_at_cursor()
                elif self.state == 'inventory':
                    mouse_x, mouse_y = event.pos
                    start_x, start_y, total_width, total_height = self._inventory_grid_layout()
                    if start_x <= mouse_x <= start_x + total_width and start_y <= mouse_y <= start_y + total_height:
                        col = (mouse_x - start_x) // (SLOT_SIZE + SLOT_MARGIN)
                        row = (mouse_y - start_y) // (SLOT_SIZE + SLOT_MARGIN)
                        if col < INVENTORY_COLS and row < INVENTORY_ROWS:
                            index = row * INVENTORY_COLS + col
                            if index < INVENTORY_SIZE:
                                self.active_slot_index = index
                                item = self.inventory[index]
                                if item is None:
                                    self.notification = 'Слот пуст'
                                    self.notification_timer = 60
                                elif getattr(item, 'edible', False):
                                    self.use_item()
                                else:
                                    self.notification = f'{item.name}: этот предмет нельзя съесть'
                                    self.notification_timer = 60
                elif self.state == 'scanner':
                    self.state = 'playing'
                    self.scanner_target = None

    def _visual_day_time(self):
        return 0.0 if self.force_midnight_preview else self.day_time

    def get_time_string(self):
        t = self._visual_day_time()
        hours = t * 24.0
        h = int(hours) % 24
        m = int((hours - int(hours)) * 60)
        if 5 <= h < 11:
            period = 'Утро'
        elif 11 <= h < 17:
            period = 'День'
        elif 17 <= h < 21:
            period = 'Вечер'
        else:
            period = 'Ночь'
        suffix = ' [F8: тест ночи]' if self.force_midnight_preview else ''
        return f'{period} {h:02d}:{m:02d}{suffix}'

    def _solar_brightness(self):
        return (1.0 - math.cos(self._visual_day_time() * math.tau)) * 0.5

    def _is_dark_night(self):
        """Ночь или подземелье; F8 также влияет на поведение хищников."""
        return self.in_cave or self._solar_brightness() < 0.25

    def _night_plankton_strength(self):
        """0 днём; плавный рост от заката до полуночи и спад к рассвету.

        F8 для проверки глубокой ночи автоматически влияет и на планктон.
        Темнота пещер сама по себе не включает дневной планктон.
        """
        if not NIGHT_PLANKTON_ENABLED or not getattr(self, 'setting_plankton_enabled', True):
            return 0.0
        light = self._solar_brightness()
        if light >= NIGHT_VISIBLE_BRIGHTNESS - 1e-08:
            return 0.0
        progress = max(0.0, min(1.0, (NIGHT_VISIBLE_BRIGHTNESS - light) / NIGHT_VISIBLE_BRIGHTNESS))
        return progress ** 1.18

    def _night_spawn_spark(self, radius):
        rng = self._night_rng
        x = self.player_x + rng.uniform(-radius, radius)
        y = self.player_y + rng.uniform(-radius, radius)
        return NightWaterSpark(rng, max(0.0, min(MAP_SIZE - 0.01, x)), max(0.0, min(MAP_SIZE - 0.01, y)))

    def _update_night_plankton(self):
        """Перемещаем только активные точки, обходясь без тысяч объектов мира."""
        night = self._night_plankton_strength()
        if night <= 0.001:
            self._night_sparks_visible = 0
            self._night_sparks.clear()
            return
        total = int(NIGHT_PLANKTON_MAX * night ** 1.35)
        total = max(0, min(NIGHT_PLANKTON_MAX, total))
        self._night_sparks_visible = total
        radius = self._render_view_radius + 2.5
        rng = self._night_rng
        while len(self._night_sparks) < total:
            self._night_sparks.append(self._night_spawn_spark(radius))
        px, py = (self.player_x, self.player_y)
        for spark in self._night_sparks[:total]:
            spark.phase = (spark.phase + spark.blink_speed) % math.tau
            drift_x = spark.vx + math.sin(spark.phase * 0.77) * spark.wander
            drift_y = spark.vy + math.cos(spark.phase * 1.13) * spark.wander
            if spark.flee_distance:
                dx = spark.x - px
                dy = spark.y - py
                distance_sq = dx * dx + dy * dy
                limit_sq = spark.flee_distance * spark.flee_distance
                if distance_sq < limit_sq:
                    distance = max(0.001, math.sqrt(distance_sq))
                    impulse = spark.flee_speed * (0.45 + 0.55 * (1.0 - distance / spark.flee_distance))
                    if distance_sq < 1e-06:
                        dx, dy = (math.cos(spark.phase), math.sin(spark.phase))
                        distance = 1.0
                    drift_x += dx / distance * impulse
                    drift_y += dy / distance * impulse
            spark.x += drift_x
            spark.y += drift_y
            offset_x, offset_y = (spark.x - px, spark.y - py)
            if abs(offset_x) > radius or abs(offset_y) > radius:
                if abs(offset_x) > radius * 2 or abs(offset_y) > radius * 2:
                    spark.x = px + rng.uniform(-radius, radius)
                    spark.y = py + rng.uniform(-radius, radius)
                elif abs(offset_x) > radius:
                    spark.x = px + (-radius if offset_x > 0 else radius)
                    spark.y = py + rng.uniform(-radius, radius)
                else:
                    spark.y = py + (-radius if offset_y > 0 else radius)
                    spark.x = px + rng.uniform(-radius, radius)
            spark.x = max(0.0, min(MAP_SIZE - 0.01, spark.x))
            spark.y = max(0.0, min(MAP_SIZE - 0.01, spark.y))

    def draw_night_plankton(self):
        """Мерцающие точки поверх ночи, но ниже HUD и без перекрытия игрока.

        Рисуем в реальном разрешении окна после LOD-прохода. Так светящиеся
        мальки сохраняют чёткость и при приближении, и при отдалении.
        """
        if self._night_sparks_visible <= 0:
            return
        strength = self._night_plankton_strength()
        if strength <= 0.001:
            return
        size = self.screen.get_size()
        if self._night_spark_surface is None or self._night_spark_surface.get_size() != size:
            self._night_spark_surface = pygame.Surface(size, pygame.SRCALPHA).convert_alpha()
        layer = self._night_spark_surface
        layer.fill((0, 0, 0, 0))
        zoom = self.camera_zoom
        pixels_per_cell = CELL_SIZE * zoom
        cx = size[0] * 0.5
        cy = size[1] * 0.5
        margin = 7
        rngless_flicker = self.frame_count * 0.026
        if self.active_vehicle is not None:
            sprite = self.get_vehicle_rotated(self.active_vehicle, self.player_angle)
        else:
            sprite = self.get_diver_rotated(self.player_angle)
        protected = pygame.Rect(0, 0, 1, 1) if sprite is None else sprite.get_rect()
        if sprite is not None:
            protected.size = (max(1, round(sprite.get_width() * zoom)), max(1, round(sprite.get_height() * zoom)))
        protected.center = (round(cx), round(cy))
        protected.inflate_ip(3, 3)
        objects = self._get_current_object_grid()
        for spark in self._night_sparks[:self._night_sparks_visible]:
            sx = int(cx + (spark.x - self.player_x) * pixels_per_cell + math.sin(spark.phase * 7.2) * spark.jitter * zoom)
            sy = int(cy + (spark.y - self.player_y) * pixels_per_cell + math.cos(spark.phase * 6.7) * spark.jitter * zoom)
            if not (margin <= sx < size[0] - margin and margin <= sy < size[1] - margin):
                continue
            if protected.collidepoint(sx, sy):
                continue
            gx, gy = (int(spark.x), int(spark.y))
            if self.in_cave:
                if self.get_cave_tunnel_at(gx, gy) is None:
                    continue
            elif self.is_inside_start_capsule(gx, gy) or not self.world.is_water_at(gx, gy):
                continue
            obj = objects.get((gx, gy))
            if obj is not None and getattr(obj, 'is_buildable', False):
                continue
            flicker = 0.5 + 0.5 * math.sin(spark.phase * 2.9 + rngless_flicker)
            flash = max(0.0, math.sin(spark.phase * 5.1 + rngless_flicker * 1.7)) ** 10
            twinkle = 0.1 + 0.65 * flicker + 0.25 * flash
            if twinkle < 0.18:
                continue
            alpha = int(232 * strength ** 0.74 * twinkle)
            if alpha < 9:
                continue
            radius_px = max(1, min(8, round(spark.radius * zoom)))
            color = spark.color
            pygame.draw.circle(layer, (*color, max(3, alpha // 9)), (sx, sy), radius_px + 4)
            pygame.draw.circle(layer, (*color, min(220, alpha // 3)), (sx, sy), radius_px + 2)
            pygame.draw.circle(layer, (*color, alpha), (sx, sy), radius_px)
            if spark.fry and radius_px >= 2:
                speed = math.hypot(spark.vx, spark.vy) or 0.001
                tail = max(2, round(radius_px * 1.7))
                tx = round(sx - spark.vx / speed * tail)
                ty = round(sy - spark.vy / speed * tail)
                pygame.draw.line(layer, (*color, alpha // 3), (sx, sy), (tx, ty), max(1, radius_px // 2))
            if flash > 0.55:
                pygame.draw.circle(layer, (235, 250, 255, int(alpha * flash)), (sx, sy), max(1, radius_px // 2))
        self.screen.blit(layer, (0, 0))

    @staticmethod
    def _make_light_source(x, y, radius, power=255, angle=None, width=40, color=(205, 230, 255), kind='lamp', reacts=True):
        """Координаты и размеры в КЛЕТКАХ, угол и ширина — в градусах."""
        return {'x': float(x), 'y': float(y), 'radius': float(radius), 'power': max(0, min(255, int(power))), 'angle': angle, 'width': width, 'color': color, 'kind': kind, 'reacts': reacts}

    def _collect_light_sources(self):
        """Общий источник данных для отрисовки света и реакции существ.

        Сканирование ограничено видимыми клетками + несколькими вокруг;
        не обходит гигантскую карту и не создаёт новых построек.
        """
        if not LIGHTING_ENABLED:
            return []
        out = []
        add = out.append
        px, py = (self.player_x, self.player_y)
        radius_view = VIEW_RADIUS + 9
        if not self.in_cave:
            x1, y1, x2, y2 = self.start_capsule_rect
            if x1 - radius_view < px < x2 + radius_view and y1 - radius_view < py < y2 + radius_view:
                cy = (y1 + y2) * 0.5
                add(self._make_light_source(x1 + 2, cy, 4.3, 255, color=(220, 242, 255), kind='capsule', reacts=False))
                add(self._make_light_source(x2 - 2, cy, 4.3, 255, color=(220, 242, 255), kind='capsule', reacts=False))
                door_cx = sum((p[0] for p in self.start_capsule_door_cells)) / 2
                add(self._make_light_source(door_cx, y2 + 0.35, 5.0, 235, angle=90, width=45, color=(255, 228, 167), kind='capsule_beacon'))
                for bx in (x1 + 0.45, x2 + 0.55):
                    add(self._make_light_source(bx, y1 + 0.5, 1.8, 205, color=(160, 220, 255), kind='capsule_beacon'))
        grid = self._get_current_object_grid()
        cx = int(px)
        cy = int(py)
        xr = range(max(0, cx - radius_view), min(MAP_SIZE, cx + radius_view + 1))
        yr = range(max(0, cy - radius_view), min(MAP_SIZE, cy + radius_view + 1))
        interior_names = {'Многоцелевая комната', 'Большая комната', 'Комната сканирования', 'Обсерватория', 'Стыковочная шахта', 'Прямой отсек', 'Угловой отсек', 'T-образный отсек', 'Крестообразный отсек', 'Прямой стеклянный отсек', 'Угловой стеклянный отсек', 'Стеклянный купол многоцелевой комнаты', 'Стеклянный купол большой комнаты', 'Вертикальный соединитель', 'Люк', 'Окно', 'Фундамент', 'Пол'}
        for iy in yr:
            for ix in xr:
                obj = grid.get((ix, iy))
                if obj is None:
                    continue
                x = ix
                y = iy
                if obj is spotlight_type:
                    angle = (ix * 73 + iy * 31) % 360
                    nearest_sq = 10.0 * 10.0
                    for animal in self.get_nearby_animals(VIEW_RADIUS + 3):
                        if animal.hp <= 0 or getattr(animal, 'is_companion', False):
                            continue
                        d2 = (animal.x - x) ** 2 + (animal.y - y) ** 2
                        if 0.01 < d2 < nearest_sq:
                            nearest_sq = d2
                            angle = math.degrees(math.atan2(animal.y - y, animal.x - x))
                    add(self._make_light_source(x, y, 8.5, 255, angle=angle, width=34, color=(255, 246, 193), kind='spotlight'))
                    add(self._make_light_source(x, y, 1.4, 230, color=(255, 242, 204), kind='lamp_near'))
                elif obj is floodlight_type:
                    add(self._make_light_source(x, y, 6.3, 248, color=(255, 238, 185), kind='floodlight'))
                elif obj in (seamoth_type, cyclops_type):
                    if self.vehicle_headlights_on:
                        add(self._make_light_source(x, y, 5.0, 220, angle=0, width=42, kind='vehicle_parked'))
                    add(self._make_light_source(x, y, 1.9, 185, color=(105, 210, 240), kind='engine', reacts=False))
                elif getattr(obj, 'name', '') in interior_names:
                    size = 3.5 if obj.name not in ('Фундамент', 'Пол') else 1.8
                    add(self._make_light_source(x, y, size, 215, color=(219, 240, 255), kind='room', reacts=False))
                elif getattr(obj, 'is_buildable', False) and obj.name in ('Изготовитель', 'Модификационная станция', 'Биореактор', 'Ядерный реактор', 'Кофейный торговый автомат', 'Радио', 'Водоочистная станция', 'Изготовитель аптечек', 'Консоль улучшения транспорта', 'Зарядное устройство батарей', 'Зарядное устройство энергоячеек'):
                    add(self._make_light_source(x, y, 1.7, 145, color=(135, 210, 235), kind='console', reacts=False))
        if self.active_vehicle is not None:
            if self.vehicle_headlights_on:
                angle = self.player_angle
                rad = math.radians(angle)
                forward = 1.0 if self.active_vehicle is seamoth_type else 2.1
                nose_x = px + math.cos(rad) * forward
                nose_y = py + math.sin(rad) * forward
                beam_r = 7.7 if self.active_vehicle is seamoth_type else 9.0
                add(self._make_light_source(nose_x, nose_y, beam_r, 255, angle=angle, width=35, color=(205, 240, 255), kind='vehicle_headlight'))
                add(self._make_light_source(nose_x, nose_y, 2.1, 225, color=(205, 243, 255), kind='vehicle_bulb'))
            add(self._make_light_source(px, py, 2.2, 185, color=(80, 195, 230), kind='engine', reacts=False))
        else:
            if self.flashlight_on:
                rad = math.radians(self.player_angle)
                add(self._make_light_source(px + math.cos(rad) * 0.3, py + math.sin(rad) * 0.3, 5.1, 255, angle=self.player_angle, width=33, color=(230, 247, 255), kind='flashlight'))
                add(self._make_light_source(px, py, 0.85, 230, color=(215, 235, 255), kind='flashlight_bulb'))
            if self.seaglide_on:
                add(self._make_light_source(px, py, 3.0, 216, color=(140, 210, 255), kind='seaglide'))
            if self.active_flare_timer > 0:
                flicker = 0.94 + 0.06 * math.sin(self.frame_count * 0.27)
                add(self._make_light_source(px, py, 4.2, int(253 * flicker), color=(255, 112, 62), kind='flare'))
        return out

    def _radial_light_sprite(self, radius_px, power):
        """Кэшируемая круговая альфа-маска с плавным спадом яркости."""
        radius_px = max(2, int(radius_px))
        power = max(0, min(255, int(power)))
        key = (radius_px, power)
        surf = self._radial_light_cache.get(key)
        if surf is not None:
            return surf
        surf = pygame.Surface((radius_px * 2 + 2, radius_px * 2 + 2), pygame.SRCALPHA)
        c = radius_px + 1
        steps = max(24, min(80, radius_px // 2))
        for step in range(steps, -1, -1):
            f = step / max(1, steps)
            r = max(1, int(radius_px * f))
            intensity = int(power * max(0, 1 - f ** 1.65))
            pygame.draw.circle(surf, (0, 0, 0, intensity), (c, c), r)
        self._radial_light_cache[key] = surf
        return surf

    def _beam_light_sprite(self, radius_px, power, angle, width):
        """Кэшированный конус: затухает к концу и боковым границам."""
        radius_px = max(3, int(radius_px))
        power = min(255, max(0, int(power)))
        angle_bin = round(float(angle) / 15) * 15 % 360
        width_bin = int(round(float(width) / 5) * 5)
        key = (radius_px, power, angle_bin, width_bin)
        cached = self._beam_light_cache.get(key)
        if cached is not None:
            return cached
        R = radius_px
        surface = pygame.Surface((2 * R + 2, 2 * R + 2), pygame.SRCALPHA)
        c = R + 1
        rings = 32
        half = math.radians(width_bin)
        for i in range(rings, -1, -1):
            frac = max(0.015, i / rings)
            rad = max(1, int(R * frac))
            distance_fade = 1.0 - frac ** 1.7
            for angular_fraction, angular_power in ((1.0, 0.4), (0.85, 0.69), (0.65, 1.0)):
                points = [(c, c)]
                for j in range(13):
                    a = -half * angular_fraction + 2 * half * angular_fraction * j / 12
                    points.append((int(c + rad * math.cos(a)), int(c + rad * math.sin(a))))
                light = int(power * distance_fade * angular_power)
                pygame.draw.polygon(surface, (0, 0, 0, light), points)
        if angle_bin:
            surface = pygame.transform.rotate(surface, -angle_bin)
        if len(self._beam_light_cache) > 110:
            self._beam_light_cache.clear()
        self._beam_light_cache[key] = surface
        return surface

    def _light_hits_creature(self, source, ax, ay):
        dx = ax - source['x']
        dy = ay - source['y']
        ds = dx * dx + dy * dy
        if ds > source['radius'] ** 2:
            return 0.0
        if source['angle'] is not None and ds > 0.35:
            aim = math.radians(source['angle'])
            cos_angle = (dx * math.cos(aim) + dy * math.sin(aim)) / max(0.001, math.sqrt(ds))
            if cos_angle < math.cos(math.radians(source['width'])):
                return 0.0
        return max(0.0, 1.0 - math.sqrt(ds) / max(0.01, source['radius'])) * (source['power'] / 255)

    def _apply_light_behavior(self, animal, world, sources):
        """Притяжение и бегство без телепортации и прохода сквозь стены."""
        if getattr(animal, 'is_companion', False) or animal.hp <= 0:
            return False
        name = animal.type.name
        attraction = LIGHT_ATTRACTION.get(name, 0.0)
        aversion = LIGHT_AVERSION.get(name, 0.0)
        if not (attraction or aversion):
            return False
        nearest = None
        score = 0.0
        for source in sources:
            if not source['reacts']:
                continue
            if source['kind'] == 'flare' and FLARES_DO_NOT_AFFECT_PREDATORS_AT_NIGHT and self._is_dark_night():
                continue
            effect = self._light_hits_creature(source, animal.x, animal.y)
            if effect > score:
                score, nearest = (effect, source)
        if nearest is None or score < 0.035:
            return False
        dx = nearest['x'] - animal.x
        dy = nearest['y'] - animal.y
        dist = math.hypot(dx, dy)
        if dist < 0.05:
            return False
        sign = -1 if aversion else 1
        sensitivity = aversion or attraction
        speed = animal.type.speed
        factor = min(1.0, 0.55 + score * 0.9) * sensitivity
        ux, uy = (dx / dist * sign, dy / dist * sign)
        old_x, old_y = (animal.x, animal.y)
        nx = old_x + ux * speed * (0.5 + factor)
        ny = old_y + uy * speed * (0.5 + factor)
        if not animal.home_biome.contains(nx, ny):
            return False
        tile = world.get_object_at(int(nx), int(ny))
        if tile is not None and tile.is_wall:
            return False
        if not self.in_cave and self.is_start_capsule_wall(int(nx), int(ny)):
            return False
        animal.x, animal.y = (nx, ny)
        animal.dx, animal.dy = (ux * speed, uy * speed)
        animal.move_timer = min(animal.move_timer, 10)
        animal._update_rotation()
        if not self.in_cave:
            self.world.update_animal_index(animal, old_x, old_y)
        return bool(aversion)

    @staticmethod
    def _color_mix(color_a, color_b, t):
        t = max(0.0, min(1.0, float(t)))
        return tuple((max(0, min(255, int(round(a + (b - a) * t)))) for a, b in zip(color_a[:3], color_b[:3])))

    @staticmethod
    def _color_scale(color, factor):
        factor = max(0.0, float(factor))
        return tuple((max(0, min(255, int(round(channel * factor)))) for channel in color[:3]))

    @staticmethod
    def _stable_visual_hash(*parts):
        value = 2166136261
        for part in parts:
            for ch in str(part):
                value ^= ord(ch)
                value = value * 16777619 & 4294967295
        return value

    @staticmethod
    def _draw_starfish_shape(surface, x, y, radius, color, seed=0):
        rng = random.Random(seed)
        points = []
        inner = max(2, int(radius * 0.46))
        for i in range(10):
            ang = -math.pi / 2 + i * math.pi / 5
            rr = radius if i % 2 == 0 else inner
            rr *= rng.uniform(0.9, 1.08)
            points.append((int(x + math.cos(ang) * rr), int(y + math.sin(ang) * rr)))
        pygame.draw.polygon(surface, color, points)
        pygame.draw.polygon(surface, Game._color_scale(color, 0.65), points, 1)
        pygame.draw.circle(surface, Game._color_mix(color, (255, 240, 220), 0.35), (int(x), int(y)), max(1, radius // 3))

    def _surface_theme_for_biome(self, biome):
        if biome is None:
            return None
        cache_key = (biome.name, tuple(biome.color))
        cached = self._seabed_theme_cache.get(cache_key)
        if cached is not None:
            return cached
        name = biome.name.lower()
        base = tuple(biome.color)
        theme = {'key': 'generic', 'base_mix': Game._color_mix(base, (165, 176, 184), 0.16), 'highlight': Game._color_mix(base, (230, 238, 245), 0.22), 'shadow': Game._color_scale(base, 0.72), 'sand_ripples': False, 'pebble_range': (2, 5), 'algae_chance': 0.1, 'coral_chance': 0.08, 'starfish_chance': 0.02, 'mottle_count': (3, 5), 'coral_palette': [(188, 122, 92), (210, 160, 110), (122, 170, 170)], 'algae_palette': [(78, 112, 78), (62, 93, 67)], 'pebble_palette': [Game._color_mix(base, (185, 185, 185), 0.28), (92, 98, 103)]}
        if name in ('центральное море', 'глубоководье'):
            theme.update({'key': 'asteroid_abyss', 'base_mix': Game._color_mix(base, (4, 24, 78), 0.42), 'highlight': Game._color_mix(base, (12, 70, 155), 0.2), 'shadow': Game._color_mix(base, (2, 10, 45), 0.55), 'sand_ripples': False, 'pebble_range': (2, 4), 'algae_chance': 0.07, 'coral_chance': 0.12, 'starfish_chance': 0.01, 'mottle_count': (3, 6)})
        elif name in ('мелководье', 'озёра (внешние)'):
            theme.update({'key': 'safe_shallows', 'base_mix': Game._color_mix(base, (222, 205, 158), 0.55), 'highlight': Game._color_mix(base, (243, 233, 193), 0.5), 'shadow': Game._color_mix(base, (148, 137, 102), 0.48), 'sand_ripples': True, 'pebble_range': (4, 8), 'algae_chance': 0.18, 'coral_chance': 0.22, 'starfish_chance': 0.14, 'mottle_count': (5, 7), 'coral_palette': [(242, 123, 118), (255, 177, 97), (160, 110, 205), (116, 206, 205)], 'algae_palette': [(92, 128, 83), (63, 96, 74)], 'pebble_palette': [(177, 165, 151), (130, 121, 112), (99, 102, 105)]})
        elif name == 'леса':
            theme.update({'key': 'forest_floor', 'base_mix': Game._color_mix(base, (62, 105, 51), 0.28), 'highlight': (100, 151, 74), 'shadow': (27, 71, 38), 'sand_ripples': False, 'pebble_range': (1, 3), 'algae_chance': 0.26, 'coral_chance': 0.0, 'starfish_chance': 0.0, 'mottle_count': (5, 9), 'algae_palette': [(77, 128, 56), (112, 151, 62)], 'pebble_palette': [(88, 96, 72), (66, 80, 62)]})
        elif name == 'кельповый лес':
            theme.update({'key': 'kelp', 'base_mix': Game._color_mix(base, (95, 110, 72), 0.36), 'highlight': Game._color_mix(base, (145, 171, 96), 0.3), 'shadow': Game._color_mix(base, (52, 69, 41), 0.46), 'sand_ripples': True, 'pebble_range': (3, 6), 'algae_chance': 0.34, 'coral_chance': 0.12, 'starfish_chance': 0.06, 'mottle_count': (5, 7), 'coral_palette': [(212, 161, 78), (182, 108, 92), (121, 162, 170)], 'algae_palette': [(74, 118, 54), (98, 140, 66), (55, 88, 48)], 'pebble_palette': [(126, 122, 102), (98, 102, 87), (75, 79, 68)]})
        elif name == 'био-люминесцентная зона':
            theme.update({'key': 'mushroom', 'base_mix': Game._color_mix(base, (77, 104, 150), 0.42), 'highlight': Game._color_mix(base, (126, 150, 198), 0.3), 'shadow': Game._color_mix(base, (35, 49, 77), 0.45), 'sand_ripples': False, 'pebble_range': (5, 8), 'algae_chance': 0.1, 'coral_chance': 0.14, 'starfish_chance': 0.03, 'mottle_count': (4, 6), 'coral_palette': [(126, 160, 225), (178, 123, 214), (101, 194, 186)], 'algae_palette': [(76, 108, 114), (60, 91, 100)], 'pebble_palette': [(112, 128, 148), (88, 96, 112), (67, 72, 81)]})
        elif name == 'коралловый риф':
            theme.update({'key': 'reef_plateau', 'base_mix': Game._color_mix(base, (116, 112, 124), 0.2), 'highlight': Game._color_mix(base, (183, 161, 175), 0.18), 'shadow': Game._color_mix(base, (74, 57, 69), 0.24), 'sand_ripples': False, 'pebble_range': (4, 7), 'algae_chance': 0.22, 'coral_chance': 0.24, 'starfish_chance': 0.08, 'mottle_count': (5, 7), 'coral_palette': [(234, 130, 151), (255, 181, 106), (129, 221, 186), (142, 173, 248)], 'algae_palette': [(76, 117, 88), (96, 145, 98), (63, 101, 85)], 'pebble_palette': [(150, 123, 128), (119, 93, 101), (86, 76, 83)]})
        elif name in ('горы', 'острова'):
            theme.update({'algae_chance': 0.0, 'coral_chance': 0.0, 'starfish_chance': 0.0})
            theme.update({'key': 'rocky', 'base_mix': Game._color_mix(base, (92, 88, 82), 0.18), 'highlight': Game._color_mix(base, (132, 127, 119), 0.18), 'shadow': Game._color_mix(base, (38, 37, 39), 0.24), 'sand_ripples': False, 'pebble_range': (6, 10), 'algae_chance': 0.08, 'coral_chance': 0.1, 'starfish_chance': 0.02, 'mottle_count': (4, 7), 'coral_palette': [(176, 120, 102), (129, 132, 145), (110, 164, 169)], 'algae_palette': [(78, 82, 66), (66, 74, 58)], 'pebble_palette': [(142, 137, 132), (103, 100, 96), (72, 71, 74)]})
        elif name in ('побережье', 'реки', 'притоки', 'водопады'):
            if name == 'побережье':
                theme.update({'algae_chance': 0.0, 'coral_chance': 0.0, 'starfish_chance': 0.0})
            theme.update({'key': 'dunes', 'base_mix': Game._color_mix(base, (208, 188, 132), 0.34), 'highlight': Game._color_mix(base, (235, 218, 174), 0.3), 'shadow': Game._color_mix(base, (144, 128, 91), 0.28), 'sand_ripples': True, 'pebble_range': (3, 6), 'algae_chance': 0.05, 'coral_chance': 0.07, 'starfish_chance': 0.05, 'mottle_count': (5, 7), 'coral_palette': [(225, 150, 99), (204, 131, 115), (112, 176, 180)], 'algae_palette': [(90, 105, 70), (66, 84, 58)], 'pebble_palette': [(179, 166, 140), (138, 125, 103), (111, 103, 87)]})
        if is_land_biome(biome):
            theme['coral_chance'] = 0.0
            theme['starfish_chance'] = 0.0
            theme['algae_chance'] = 0.0
        self._seabed_theme_cache[cache_key] = theme
        return theme

    def _build_seabed_tile_surface(self, biome, variant):
        theme = self._surface_theme_for_biome(biome)
        if theme is None:
            return None
        rng = random.Random(variant * 9173 + self._stable_visual_hash(biome.name, theme['key']))
        surf = pygame.Surface((CELL_SIZE, CELL_SIZE), pygame.SRCALPHA)
        surf.fill(theme['base_mix'])
        for y in range(CELL_SIZE):
            frac = y / max(1, CELL_SIZE - 1)
            top_color = theme['highlight']
            low_color = theme['shadow']
            row_color = Game._color_mix(top_color, low_color, frac * 0.65)
            alpha = 36
            pygame.draw.line(surf, (*row_color, alpha), (0, y), (CELL_SIZE, y))
        mottle_min, mottle_max = theme['mottle_count']
        for _ in range(rng.randint(mottle_min, mottle_max)):
            w = rng.randint(8, 18)
            h = rng.randint(5, 14)
            x = rng.randint(-3, CELL_SIZE - 4)
            y = rng.randint(-3, CELL_SIZE - 4)
            tint = theme['highlight'] if rng.random() < 0.45 else theme['shadow']
            alpha = rng.randint(26, 52)
            pygame.draw.ellipse(surf, (*tint, alpha), (x, y, w, h))
        if theme['sand_ripples']:
            ripple = Game._color_mix(theme['shadow'], theme['highlight'], 0.4)
            for _ in range(rng.randint(2, 4)):
                base_y = rng.randint(4, CELL_SIZE - 5)
                phase = rng.uniform(0.0, math.tau)
                amp = rng.uniform(0.8, 1.8)
                pts = []
                for xx in range(-2, CELL_SIZE + 3, 3):
                    yy = base_y + math.sin(xx * 0.33 + phase) * amp
                    pts.append((xx, int(round(yy))))
                pygame.draw.lines(surf, (*ripple, 58), False, pts, 1)
        pebble_count = rng.randint(*theme['pebble_range'])
        for _ in range(pebble_count):
            px = rng.randint(2, CELL_SIZE - 3)
            py = rng.randint(2, CELL_SIZE - 3)
            pw = rng.randint(2, 5)
            ph = rng.randint(2, 4)
            pcol = rng.choice(theme['pebble_palette'])
            rect = pygame.Rect(px - pw // 2, py - ph // 2, pw, ph)
            pygame.draw.ellipse(surf, pcol, rect)
            pygame.draw.ellipse(surf, Game._color_mix(pcol, (240, 240, 240), 0.22), rect.inflate(-1, -1))
        if rng.random() < theme['algae_chance']:
            clump_x = rng.randint(5, CELL_SIZE - 6)
            clump_y = rng.randint(6, CELL_SIZE - 5)
            base_y = min(CELL_SIZE - 3, clump_y + rng.randint(2, 5))
            for _ in range(rng.randint(4, 7)):
                height = rng.randint(6, 12)
                bend = rng.randint(-4, 4)
                tip_x = clump_x + bend + rng.randint(-3, 3)
                tip_y = max(2, base_y - height)
                col = rng.choice(theme['algae_palette'])
                mid_x = (clump_x + tip_x) // 2 + rng.randint(-2, 2)
                mid_y = base_y - height // 2
                pygame.draw.lines(surf, (*col, 200), False, [(clump_x, base_y), (mid_x, mid_y), (tip_x, tip_y)], 1)
                pygame.draw.circle(surf, Game._color_mix(col, (220, 240, 210), 0.16), (tip_x, tip_y), 1)
        if rng.random() < theme['coral_chance']:
            coral_x = rng.randint(5, CELL_SIZE - 6)
            coral_y = rng.randint(5, CELL_SIZE - 6)
            coral_color = rng.choice(theme['coral_palette'])
            style_roll = rng.random()
            if style_roll < 0.34:
                base_point = (coral_x, coral_y + 3)
                for _ in range(rng.randint(3, 5)):
                    tip = (coral_x + rng.randint(-5, 5), coral_y - rng.randint(2, 6))
                    mid = ((base_point[0] + tip[0]) // 2 + rng.randint(-2, 2), (base_point[1] + tip[1]) // 2 - rng.randint(0, 2))
                    pygame.draw.lines(surf, coral_color, False, [base_point, mid, tip], 2)
                    pygame.draw.circle(surf, Game._color_mix(coral_color, (250, 240, 215), 0.28), tip, 1)
            elif style_roll < 0.67:
                for _ in range(rng.randint(2, 4)):
                    rr = rng.randint(2, 4)
                    ox = rng.randint(-3, 3)
                    oy = rng.randint(-3, 3)
                    pygame.draw.circle(surf, coral_color, (coral_x + ox, coral_y + oy), rr)
                    pygame.draw.circle(surf, Game._color_mix(coral_color, (255, 240, 230), 0.22), (coral_x + ox - 1, coral_y + oy - 1), max(1, rr - 1))
            else:
                rect = pygame.Rect(coral_x - 5, coral_y - 3, 11, 6)
                pygame.draw.ellipse(surf, coral_color, rect)
                pygame.draw.ellipse(surf, Game._color_scale(coral_color, 0.68), rect, 1)
                pygame.draw.line(surf, Game._color_mix(coral_color, (250, 240, 225), 0.25), (rect.left + 2, coral_y), (rect.right - 2, coral_y), 1)
        if rng.random() < theme['starfish_chance']:
            sx = rng.randint(6, CELL_SIZE - 7)
            sy = rng.randint(6, CELL_SIZE - 7)
            star_color = rng.choice([(227, 102, 86), (250, 172, 77), (189, 114, 210)])
            self._draw_starfish_shape(surf, sx, sy, rng.randint(4, 5), star_color, seed=variant * 13 + sx + sy)
        grit = Game._color_mix(theme['highlight'], (230, 225, 210), 0.3)
        for _ in range(rng.randint(6, 11)):
            gx = rng.randint(0, CELL_SIZE - 1)
            gy = rng.randint(0, CELL_SIZE - 1)
            surf.set_at((gx, gy), (*grit, 105))
        return surf

    def _get_seabed_tile_surface(self, biome, x, y):
        if biome is None:
            return None
        theme = self._surface_theme_for_biome(biome)
        if theme is None:
            return None
        variant = self._stable_visual_hash(biome.name, x // 2, y // 2, x % 3, y % 3) % 24
        key = (theme['key'], tuple(theme['base_mix']), tuple(theme['highlight']), tuple(theme['shadow']), variant)
        tile = self._seabed_tile_cache.get(key)
        if tile is None:
            tile = self._build_seabed_tile_surface(biome, variant)
            self._seabed_tile_cache[key] = tile
        return tile

    def _cave_theme_for_tunnel(self, tunnel):
        if tunnel is None:
            return {'key': 'solid_wall', 'base_mix': (18, 18, 23), 'highlight': (40, 40, 49), 'shadow': (8, 8, 11), 'mottle_count': (5, 8), 'pebble_range': (2, 5), 'crystal_chance': 0.1, 'fungus_chance': 0.02, 'fossil_chance': 0.03, 'mineral_palette': [(80, 102, 132), (72, 84, 98), (90, 78, 68)]}
        parent = getattr(tunnel, 'parent_surface_biome', None)
        parent_name = parent.name if parent is not None else 'interbiome'
        cache_key = (tunnel.name, tuple(tunnel.color), parent_name, getattr(tunnel, 'cave_kind', ''))
        cached = self._cave_theme_cache.get(cache_key)
        if cached is not None:
            return cached
        base = tuple(tunnel.color)
        name = parent_name.lower()
        kind = getattr(tunnel, 'cave_kind', '')
        theme = {'key': 'generic_cave', 'base_mix': Game._color_mix(base, (68, 77, 88), 0.24), 'highlight': Game._color_mix(base, (126, 140, 158), 0.16), 'shadow': Game._color_mix(base, (14, 18, 25), 0.44), 'mottle_count': (4, 6), 'pebble_range': (4, 8), 'crystal_chance': 0.14, 'fungus_chance': 0.08, 'fossil_chance': 0.06, 'mineral_palette': [(119, 146, 186), (106, 128, 149), (122, 109, 95)]}
        if 'безопасные отмели' in name:
            theme.update({'key': 'shallow_cave', 'base_mix': Game._color_mix(base, (101, 88, 76), 0.24), 'highlight': Game._color_mix(base, (158, 141, 120), 0.18), 'shadow': Game._color_mix(base, (28, 24, 24), 0.4), 'mottle_count': (5, 7), 'pebble_range': (5, 9), 'crystal_chance': 0.1, 'fungus_chance': 0.04, 'fossil_chance': 0.08, 'mineral_palette': [(145, 128, 103), (98, 94, 90), (122, 154, 171)]})
        elif 'грибной лес' in name or 'большой риф' in name or 'редкий риф' in name:
            theme.update({'key': 'fungal_cave', 'base_mix': Game._color_mix(base, (74, 83, 109), 0.26), 'highlight': Game._color_mix(base, (138, 155, 192), 0.18), 'shadow': Game._color_mix(base, (18, 20, 32), 0.42), 'crystal_chance': 0.19, 'fungus_chance': 0.18, 'fossil_chance': 0.05, 'mineral_palette': [(145, 176, 229), (168, 116, 206), (115, 188, 184)]})
        elif 'бурые водоросли' in name or 'водоросли' == name or 'зона куша' in name:
            theme.update({'key': 'kelp_cave', 'base_mix': Game._color_mix(base, (76, 84, 63), 0.24), 'highlight': Game._color_mix(base, (118, 138, 96), 0.16), 'shadow': Game._color_mix(base, (18, 24, 18), 0.44), 'crystal_chance': 0.1, 'fungus_chance': 0.1, 'fossil_chance': 0.05, 'mineral_palette': [(122, 136, 96), (84, 92, 72), (113, 154, 120)]})
        elif 'горы' in name or 'скалистое поле' in name or 'зона крушения' in name or ('подводные острова' in name):
            theme.update({'key': 'rock_cave', 'base_mix': Game._color_mix(base, (82, 78, 82), 0.18), 'highlight': Game._color_mix(base, (132, 126, 136), 0.14), 'shadow': Game._color_mix(base, (13, 14, 18), 0.48), 'mottle_count': (5, 8), 'pebble_range': (6, 10), 'crystal_chance': 0.09, 'fungus_chance': 0.04, 'fossil_chance': 0.1, 'mineral_palette': [(122, 124, 136), (92, 94, 104), (126, 110, 96)]})
        elif 'дюны' in name or 'морской путь' in name:
            theme.update({'key': 'dune_cave', 'base_mix': Game._color_mix(base, (118, 103, 76), 0.24), 'highlight': Game._color_mix(base, (176, 158, 114), 0.16), 'shadow': Game._color_mix(base, (30, 24, 18), 0.44), 'crystal_chance': 0.07, 'fungus_chance': 0.03, 'fossil_chance': 0.1, 'mineral_palette': [(150, 129, 91), (124, 111, 88), (162, 166, 154)]})
        if kind in ('hub', 'branch_room'):
            theme['highlight'] = Game._color_mix(theme['highlight'], (190, 206, 220), 0.14)
            theme['crystal_chance'] = min(0.24, theme['crystal_chance'] + 0.04)
            theme['fungus_chance'] = min(0.22, theme['fungus_chance'] + 0.03)
            theme['mottle_count'] = (theme['mottle_count'][0] + 1, theme['mottle_count'][1] + 1)
        elif kind in ('interbiome_corridor', 'local_corridor'):
            theme['shadow'] = Game._color_mix(theme['shadow'], (7, 8, 10), 0.18)
            theme['crystal_chance'] = max(0.05, theme['crystal_chance'] - 0.03)
            theme['fungus_chance'] = max(0.02, theme['fungus_chance'] - 0.02)
        elif kind == 'interbiome_junction':
            theme['highlight'] = Game._color_mix(theme['highlight'], (166, 174, 188), 0.1)
            theme['fossil_chance'] = min(0.12, theme['fossil_chance'] + 0.03)
        self._cave_theme_cache[cache_key] = theme
        return theme

    def _build_cave_floor_tile_surface(self, tunnel, variant):
        theme = self._cave_theme_for_tunnel(tunnel)
        if theme is None:
            return None
        seed = self._stable_visual_hash(tunnel.name, theme['key'], variant)
        rng = random.Random(seed)
        surf = pygame.Surface((CELL_SIZE, CELL_SIZE), pygame.SRCALPHA)
        surf.fill(theme['base_mix'])
        for y in range(CELL_SIZE):
            frac = y / max(1, CELL_SIZE - 1)
            row_color = Game._color_mix(theme['highlight'], theme['shadow'], frac * 0.8)
            alpha = 40 + int(18 * frac)
            pygame.draw.line(surf, (*row_color, alpha), (0, y), (CELL_SIZE, y))
        mottle_min, mottle_max = theme['mottle_count']
        for _ in range(rng.randint(mottle_min, mottle_max)):
            w = rng.randint(6, 18)
            h = rng.randint(5, 14)
            x = rng.randint(-4, CELL_SIZE - 4)
            y = rng.randint(-3, CELL_SIZE - 4)
            tint = theme['highlight'] if rng.random() < 0.42 else theme['shadow']
            alpha = rng.randint(24, 54)
            pygame.draw.ellipse(surf, (*tint, alpha), (x, y, w, h))
        vein_color = Game._color_mix(theme['shadow'], theme['highlight'], 0.3)
        for _ in range(rng.randint(2, 4)):
            sx = rng.randint(-2, CELL_SIZE // 3)
            sy = rng.randint(1, CELL_SIZE - 2)
            pts = [(sx, sy)]
            cx = sx
            cy = sy
            for _step in range(rng.randint(2, 4)):
                cx += rng.randint(3, 7)
                cy += rng.randint(-3, 3)
                pts.append((max(-2, min(CELL_SIZE + 2, cx)), max(0, min(CELL_SIZE - 1, cy))))
            pygame.draw.lines(surf, (*vein_color, 65), False, pts, 1)
        for _ in range(rng.randint(*theme['pebble_range'])):
            px = rng.randint(2, CELL_SIZE - 3)
            py = rng.randint(2, CELL_SIZE - 3)
            pw = rng.randint(2, 5)
            ph = rng.randint(2, 4)
            pcol = rng.choice(theme['mineral_palette'])
            rect = pygame.Rect(px - pw // 2, py - ph // 2, pw, ph)
            pygame.draw.ellipse(surf, pcol, rect)
            inner = rect.inflate(-1, -1)
            if inner.width > 0 and inner.height > 0:
                pygame.draw.ellipse(surf, Game._color_mix(pcol, (222, 226, 232), 0.2), inner)
        if rng.random() < 0.22:
            base_x = rng.randint(5, CELL_SIZE - 6)
            base_y = rng.randint(CELL_SIZE // 2, CELL_SIZE - 3)
            count = rng.randint(2, 4)
            ridge_col = Game._color_mix(theme['shadow'], theme['base_mix'], 0.45)
            for idx in range(count):
                height = rng.randint(5, 10)
                width = rng.randint(3, 5)
                cx = base_x + idx * rng.randint(2, 4) - count
                pts = [(cx - width // 2, base_y), (cx, base_y - height), (cx + width // 2, base_y)]
                pygame.draw.polygon(surf, ridge_col, pts)
                pygame.draw.line(surf, Game._color_mix(ridge_col, (190, 195, 205), 0.18), pts[0], pts[1], 1)
        if rng.random() < theme['crystal_chance']:
            crystal_color = rng.choice(theme['mineral_palette'])
            crystal_color = Game._color_mix(crystal_color, (185, 220, 255), 0.24)
            origin_x = rng.randint(4, CELL_SIZE - 5)
            origin_y = rng.randint(4, CELL_SIZE - 5)
            for _ in range(rng.randint(2, 4)):
                cx = origin_x + rng.randint(-4, 4)
                cy = origin_y + rng.randint(-3, 3)
                h = rng.randint(4, 8)
                w = rng.randint(2, 4)
                pts = [(cx, cy - h), (cx + w, cy), (cx, cy + 1), (cx - w, cy)]
                pygame.draw.polygon(surf, crystal_color, pts)
                pygame.draw.polygon(surf, Game._color_scale(crystal_color, 0.55), pts, 1)
                pygame.draw.line(surf, Game._color_mix(crystal_color, (245, 250, 255), 0.45), (cx, cy - h + 1), (cx, cy), 1)
        if rng.random() < theme['fungus_chance']:
            stem = Game._color_mix(theme['highlight'], (208, 214, 224), 0.22)
            cap = Game._color_mix(rng.choice(theme['mineral_palette']), (208, 170, 224), 0.3)
            base_x = rng.randint(5, CELL_SIZE - 6)
            base_y = rng.randint(7, CELL_SIZE - 5)
            for _ in range(rng.randint(2, 3)):
                cx = base_x + rng.randint(-4, 4)
                h = rng.randint(4, 7)
                pygame.draw.line(surf, stem, (cx, base_y), (cx, base_y - h), 1)
                rect = pygame.Rect(cx - 3, base_y - h - 2, 7, 4)
                pygame.draw.ellipse(surf, cap, rect)
                pygame.draw.ellipse(surf, Game._color_scale(cap, 0.6), rect, 1)
        if rng.random() < theme['fossil_chance']:
            fossil = Game._color_mix(theme['shadow'], theme['highlight'], 0.55)
            fx = rng.randint(6, CELL_SIZE - 7)
            fy = rng.randint(6, CELL_SIZE - 7)
            style = rng.random()
            if style < 0.5:
                pygame.draw.arc(surf, (*fossil, 130), (fx - 5, fy - 4, 10, 8), math.pi * 0.15, math.pi * 1.75, 1)
                pygame.draw.arc(surf, (*fossil, 90), (fx - 3, fy - 2, 6, 4), math.pi * 0.15, math.pi * 1.75, 1)
            else:
                for off in range(-3, 4, 2):
                    pygame.draw.line(surf, (*fossil, 120), (fx - 4, fy + off), (fx + 4, fy + off // 2), 1)
        grit = Game._color_mix(theme['highlight'], (220, 220, 225), 0.24)
        for _ in range(rng.randint(6, 12)):
            gx = rng.randint(0, CELL_SIZE - 1)
            gy = rng.randint(0, CELL_SIZE - 1)
            surf.set_at((gx, gy), (*grit, 95))
        return surf

    def _build_cave_wall_tile_surface(self, variant):
        rng = random.Random(310000 + variant * 271)
        surf = pygame.Surface((CELL_SIZE, CELL_SIZE), pygame.SRCALPHA)
        base = (13, 13, 18)
        high = (34, 34, 42)
        deep = (5, 5, 8)
        surf.fill(base)
        for y in range(CELL_SIZE):
            frac = y / max(1, CELL_SIZE - 1)
            row = Game._color_mix(high, deep, frac * 0.78)
            pygame.draw.line(surf, (*row, 42), (0, y), (CELL_SIZE, y))
        for _ in range(rng.randint(6, 10)):
            x = rng.randint(-4, CELL_SIZE - 4)
            y = rng.randint(-3, CELL_SIZE - 4)
            w = rng.randint(7, 16)
            h = rng.randint(5, 14)
            col = high if rng.random() < 0.42 else deep
            pygame.draw.ellipse(surf, (*col, rng.randint(24, 52)), (x, y, w, h))
        for _ in range(rng.randint(2, 4)):
            sx = rng.randint(0, CELL_SIZE - 1)
            sy = rng.randint(0, CELL_SIZE - 1)
            pts = [(sx, sy)]
            cx = sx
            cy = sy
            for _step in range(rng.randint(2, 5)):
                cx += rng.randint(-4, 4)
                cy += rng.randint(2, 6)
                pts.append((max(0, min(CELL_SIZE - 1, cx)), max(0, min(CELL_SIZE - 1, cy))))
            pygame.draw.lines(surf, (48, 48, 57, 90), False, pts, 1)
        for _ in range(rng.randint(5, 9)):
            cx = rng.randint(2, CELL_SIZE - 3)
            cy = rng.randint(2, CELL_SIZE - 3)
            rr = rng.randint(1, 3)
            col = rng.choice([(62, 65, 76), (78, 84, 92), (56, 48, 44)])
            pygame.draw.circle(surf, col, (cx, cy), rr)
            if rr > 1:
                pygame.draw.circle(surf, Game._color_mix(col, (170, 176, 184), 0.18), (cx - 1, cy - 1), rr - 1)
        pygame.draw.rect(surf, (0, 0, 0, 34), (0, 0, CELL_SIZE, CELL_SIZE), 2)
        return surf

    def _get_cave_floor_tile_surface(self, tunnel, x, y):
        if tunnel is None:
            return None
        theme = self._cave_theme_for_tunnel(tunnel)
        variant = self._stable_visual_hash(tunnel.name, x // 2, y // 2, x % 3, y % 3) % 24
        key = (theme['key'], tuple(theme['base_mix']), tuple(theme['highlight']), tuple(theme['shadow']), variant)
        tile = self._cave_floor_tile_cache.get(key)
        if tile is None:
            tile = self._build_cave_floor_tile_surface(tunnel, variant)
            self._cave_floor_tile_cache[key] = tile
        return tile

    def _get_cave_wall_tile_surface(self, x, y):
        variant = self._stable_visual_hash('cave_wall', x // 2, y // 2, x % 5, y % 5) % 32
        tile = self._cave_wall_tile_cache.get(variant)
        if tile is None:
            tile = self._build_cave_wall_tile_surface(variant)
            self._cave_wall_tile_cache[variant] = tile
        return tile

    def draw_cave_cell(self, x, y, screen_x, screen_y, tunnel):
        if tunnel is None:
            wall = self._get_cave_wall_tile_surface(x, y)
            self.screen.blit(wall, (screen_x, screen_y))
            return
        tile = self._get_cave_floor_tile_surface(tunnel, x, y) if getattr(self, 'setting_seabed_details_enabled', True) else None
        if tile is not None:
            self.screen.blit(tile, (screen_x, screen_y))
        else:
            pygame.draw.rect(self.screen, tunnel.color, (screen_x, screen_y, CELL_SIZE, CELL_SIZE))
        edge_shadow = pygame.Surface((CELL_SIZE, CELL_SIZE), pygame.SRCALPHA)
        neighbors = {'top': self.get_cave_tunnel_at(x, y - 1) is None, 'bottom': self.get_cave_tunnel_at(x, y + 1) is None, 'left': self.get_cave_tunnel_at(x - 1, y) is None, 'right': self.get_cave_tunnel_at(x + 1, y) is None}
        if neighbors['top']:
            pygame.draw.rect(edge_shadow, (0, 0, 0, 56), (0, 0, CELL_SIZE, 4))
        if neighbors['bottom']:
            pygame.draw.rect(edge_shadow, (0, 0, 0, 60), (0, CELL_SIZE - 4, CELL_SIZE, 4))
        if neighbors['left']:
            pygame.draw.rect(edge_shadow, (0, 0, 0, 52), (0, 0, 4, CELL_SIZE))
        if neighbors['right']:
            pygame.draw.rect(edge_shadow, (0, 0, 0, 52), (CELL_SIZE - 4, 0, 4, CELL_SIZE))
        if neighbors['top'] and neighbors['left']:
            pygame.draw.circle(edge_shadow, (0, 0, 0, 48), (2, 2), 4)
        if neighbors['top'] and neighbors['right']:
            pygame.draw.circle(edge_shadow, (0, 0, 0, 48), (CELL_SIZE - 3, 2), 4)
        if neighbors['bottom'] and neighbors['left']:
            pygame.draw.circle(edge_shadow, (0, 0, 0, 48), (2, CELL_SIZE - 3), 4)
        if neighbors['bottom'] and neighbors['right']:
            pygame.draw.circle(edge_shadow, (0, 0, 0, 48), (CELL_SIZE - 3, CELL_SIZE - 3), 4)
        self.screen.blit(edge_shadow, (screen_x, screen_y))
        border = Game._color_mix(tuple(tunnel.color), (165, 176, 190), 0.32)
        pygame.draw.rect(self.screen, border, (screen_x, screen_y, CELL_SIZE, CELL_SIZE), 1)

    def draw_land_tree(self, x, y, screen_x, screen_y):
        if self.in_cave:
            return
        tree = self.world.tree_grid.get((x, y))
        if tree is None:
            return
        biome = self.world.get_biome_at(x, y)
        if biome is None or biome.name != 'Леса':
            return
        sprite = self.tree_sprite_cache.get(tree)
        if sprite is None:
            sprite = generate_tree_sprite(*tree)
            self.tree_sprite_cache[tree] = sprite
        center = (screen_x + CELL_SIZE // 2, screen_y + CELL_SIZE // 2)
        self.screen.blit(sprite, sprite.get_rect(center=center))

    def draw_surface_seabed_cell(self, x, y, screen_x, screen_y, biome):
        if not getattr(self, 'setting_seabed_details_enabled', True):
            return
        tile = self._get_seabed_tile_surface(biome, x, y)
        if tile is not None:
            self.screen.blit(tile, (screen_x, screen_y))

    def draw_day_night_overlay(self):
        """Маски света вырезают плавные пятна и лучи в чёрной завесе."""
        if not getattr(self, 'setting_water_darkening_enabled', True):
            return
        if self.in_cave:
            alpha = 250
        else:
            brightness = self._solar_brightness()
            if MIDNIGHT_BLACKOUT and min(self._visual_day_time(), 1.0 - self._visual_day_time()) <= MIDNIGHT_BLACKOUT_HALF_WINDOW:
                alpha = 255
            else:
                twilight = max(0.0, min(1.0, (0.72 - brightness) / 0.72))
                alpha = round(255 * twilight ** 1.5)
        if alpha <= 0:
            return
        overlay = self.lighting_overlay
        overlay.fill((0, 0, 0, alpha))
        camera_x, camera_y = self.get_render_camera_origin()
        if LIGHTING_ENABLED:
            for src in self._collect_light_sources():
                sx = int((src['x'] - camera_x) * CELL_SIZE + CELL_SIZE // 2)
                sy = int((src['y'] - camera_y) * CELL_SIZE + CELL_SIZE // 2)
                radius = max(3, int(src['radius'] * CELL_SIZE))
                if sx < -radius or sy < -radius or sx > self._render_canvas_size + radius or (sy > self._render_canvas_size + radius):
                    continue
                if src['angle'] is None:
                    light = self._radial_light_sprite(radius, src['power'])
                else:
                    light = self._beam_light_sprite(radius, src['power'], src['angle'], src['width'])
                overlay.blit(light, light.get_rect(center=(sx, sy)), special_flags=pygame.BLEND_RGBA_SUB)
        self.screen.blit(overlay, (0, 0))

    def draw_light_emitters(self, camera_x, camera_y):
        """Яркие сердцевины источников видны даже среди чёрной воды."""
        if not LIGHTING_ENABLED:
            return
        for src in self._collect_light_sources():
            if src['kind'] == 'room':
                continue
            sx = int((src['x'] - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = int((src['y'] - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            if not (-20 <= sx <= self._render_canvas_size + 20 and -20 <= sy <= self._render_canvas_size + 20):
                continue
            color = src['color']
            glow = self._emitter_glow_cache.get(color)
            if glow is None:
                glow = pygame.Surface((50, 50), pygame.SRCALPHA)
                pygame.draw.circle(glow, (*color, 16), (25, 25), 23)
                pygame.draw.circle(glow, (*color, 27), (25, 25), 14)
                pygame.draw.circle(glow, (*color, 75), (25, 25), 7)
                self._emitter_glow_cache[color] = glow
            self.screen.blit(glow, (sx - 25, sy - 25))
            pygame.draw.circle(self.screen, tuple((min(255, channel + 28) for channel in color)), (sx, sy), 2)

    def _draw_one_bioglow(self, image, center, alpha):
        if image is None:
            return
        image.set_alpha(max(0, min(255, int(alpha))))
        self.screen.blit(image, image.get_rect(center=center))

    def draw_bioluminescence(self, camera_x, camera_y):
        """Динамическое свечение ВСЕХ видимых растений и животных.

        Работает и в пещерах, и с питомцами, и с высаженной флорой.
        Сканер, инвентарь и крафт не меняют свой исходный интерфейс.
        """
        if not BIOLUMINESCENCE_ENABLED:
            return
        night_strength = 1.0 if self.in_cave else max(0.0, 1.0 - self._solar_brightness() * 1.35)
        base_alpha = BIOGLOW_DAY_ALPHA + (BIOGLOW_NIGHT_ALPHA - BIOGLOW_DAY_ALPHA) * night_strength
        pulse = 0.88 + 0.12 * math.sin(self.frame_count * 0.045)
        alpha = round(base_alpha * pulse)
        start_x = max(0, int(math.floor(camera_x)) - 2)
        start_y = max(0, int(math.floor(camera_y)) - 2)
        end_x = min(MAP_SIZE - 1, int(math.ceil(camera_x + self._render_view_radius * 2)) + 2)
        end_y = min(MAP_SIZE - 1, int(math.ceil(camera_y + self._render_view_radius * 2)) + 2)
        flora_grid = self._current_flora_grid()
        object_grid = self._get_current_object_grid()
        planter_types = self._planter_types()
        for y in range(start_y, end_y + 1):
            sy = int(round((y - camera_y) * CELL_SIZE + CELL_SIZE // 2))
            for x in range(start_x, end_x + 1):
                sx = int(round((x - camera_x) * CELL_SIZE + CELL_SIZE // 2))
                flora = flora_grid.get((x, y))
                if flora is not None:
                    self._draw_one_bioglow(flora.glow_image, (sx, sy), alpha)
                obj = object_grid.get((x, y))
                if obj is not None:
                    self._draw_one_bioglow(getattr(obj, 'glow_image', None), (sx, sy), alpha)
                if obj in planter_types:
                    planted = self.planted_flora.get(self._planter_key(x, y), ())
                    offsets = ((0, 1), (-8, -3), (8, -3), (-8, 7), (8, 7), (0, -8), (0, 9))
                    max_size = 20 if len(planted) <= 2 else 15
                    for index, plant in enumerate(planted):
                        if plant.glow_image is None:
                            continue
                        key = (plant, max_size)
                        glow = self._bio_planted_scaled_cache.get(key)
                        if glow is None:
                            original = plant.image
                            scale = min(max_size / max(1, original.get_width()), max_size / max(1, original.get_height()))
                            target = (max(1, round(plant.glow_image.get_width() * scale)), max(1, round(plant.glow_image.get_height() * scale)))
                            glow = pygame.transform.smoothscale(plant.glow_image, target)
                            self._bio_planted_scaled_cache[key] = glow
                        ox, oy = offsets[index % len(offsets)]
                        self._draw_one_bioglow(glow, (sx + ox, sy + oy), alpha)
        if not self.in_cave and night_strength > 0.6:
            for moss in self.world.moss_colonies:
                if abs(moss.x - self.player_x) > self._render_view_radius + 2 or abs(moss.y - self.player_y) > self._render_view_radius + 2:
                    continue
                sx = round((moss.x - camera_x) * CELL_SIZE + CELL_SIZE // 2)
                sy = round((moss.y - camera_y) * CELL_SIZE + CELL_SIZE // 2)
                pulse = 0.7 + 0.3 * math.sin(self.frame_count * 0.09 + moss.variant * 2)
                if pulse > 0.7:
                    pygame.draw.circle(self.screen, (139, 243, 91), (sx, sy), 1)
        if not self.in_cave:
            for golem in self.world.golem_colonies:
                if golem.phase not in ('active', 'breeding', 'rubble'):
                    continue
                if abs(golem.x - self.player_x) > self._render_view_radius + 2 or abs(golem.y - self.player_y) > self._render_view_radius + 2:
                    continue
                sx = int((golem.x - camera_x) * CELL_SIZE + CELL_SIZE // 2)
                sy = int((golem.y - camera_y) * CELL_SIZE + CELL_SIZE // 2)
                gl = {'stone': (116, 240, 218), 'water': (165, 255, 255), 'wood': (139, 251, 111), 'dung': (250, 207, 72)}[golem.kind]
                radii = ((2, 0), (2, 9), (2, -9), (8, -4))
                if night_strength > 0.5:
                    for radius, dx in radii:
                        pygame.draw.circle(self.screen, gl, (sx + dx, sy), radius)
                else:
                    pygame.draw.circle(self.screen, gl, (sx, sy), 1)
        for animal in self.get_nearby_animals(self._render_view_radius + 2):
            if animal.hp <= 0:
                continue
            sx = int((animal.x - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            sy = int((animal.y - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            phase = animal.x * 0.27 + animal.y * 0.39
            creature_pulse = 0.93 + 0.07 * math.sin(self.frame_count * 0.052 + phase)
            bioglow = animal.rotated_glow or animal.type.glow_image
            self._draw_one_bioglow(bioglow, (sx, sy), round(alpha * creature_pulse))

    def update(self):
        if self.state == 'map':
            self.update_map_pan()
            self.update_notification()
            return
        if self.state != 'playing':
            self.update_notification()
            return
        if self.settings_open:
            self.update_notification()
            return
        self.day_time = (self.day_time + self.day_speed) % 1.0
        self.elapsed_game_days += self.day_speed
        while self.elapsed_game_days + 1e-09 >= self.next_ecosystem_refresh_day:
            self.refresh_ecosystem()
            self.next_ecosystem_refresh_day += self.ecosystem_refresh_interval_days
        keys = pygame.key.get_pressed()
        on_land = self._is_player_on_land()
        self.player_mode = 'walking' if on_land else 'swimming'
        if on_land and self.active_vehicle in (seamoth_type, cyclops_type):
            self.active_vehicle = None
            self.notification = 'Берег: подлодка убрана, переход на ходьбу'
            self.notification_timer = 100
        elif (not on_land) and self.active_vehicle in (light_jeep_type, armored_carrier_type):
            self.active_vehicle = None
            self.notification = 'Наземный транспорт оставлен перед водой или пещерой'
            self.notification_timer = 100
        if self.active_vehicle is seamoth_type:
            move_speed = 0.78
        elif self.active_vehicle is cyclops_type:
            move_speed = 0.46
        elif self.active_vehicle is light_jeep_type:
            move_speed = 0.78 * 1.5
        elif self.active_vehicle is armored_carrier_type:
            move_speed = 0.46
        else:
            move_speed = WALK_SPEED if on_land else 0.5
            if not on_land and self.inventory_counts.get(fins_type, 0) > 0:
                move_speed *= 1.15
            if not on_land and self.seaglide_on:
                move_speed *= 1.55
            if self.speed_boost_timer > 0:
                move_speed *= 1.35
        dx = 0
        dy = 0
        if keys[pygame.K_w] or keys[pygame.K_UP]:
            dy = -move_speed
        if keys[pygame.K_s] or keys[pygame.K_DOWN]:
            dy = move_speed
        if keys[pygame.K_a] or keys[pygame.K_LEFT]:
            dx = -move_speed
        if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
            dx = move_speed
        old_player_x = self.player_x
        old_player_y = self.player_y
        new_x = self.player_x + dx
        new_y = self.player_y + dy
        # Проходим путь малыми шагами: быстрый джип не перескакивает
        # через берег, стену или узкое препятствие между кадрами.
        steps = max(1, math.ceil(max(abs(dx), abs(dy)) * 3))
        for _step in range(steps):
            new_x = self.player_x + dx / steps
            new_y = self.player_y + dy / steps
            if not (0 <= new_x < MAP_SIZE and 0 <= new_y < MAP_SIZE):
                break
            if self.in_cave:
                if self.cave_is_passable(new_x, new_y):
                    self.player_x, self.player_y = new_x, new_y
                else:
                    break
            else:
                target_cell_x, target_cell_y = int(new_x), int(new_y)
                target_object = self.world.object_grid.get((target_cell_x, target_cell_y))
                biome = self.world.get_biome_at(target_cell_x, target_cell_y)
                object_passable = target_object is None or not target_object.is_wall
                capsule_wall = self.is_start_capsule_wall(target_cell_x, target_cell_y)
                if self.is_start_capsule_passage(target_cell_x, target_cell_y):
                    biome_passable = True
                else:
                    biome_passable = biome is not None and biome.passable
                vehicle_can_pass = (self.active_vehicle is None or
                    (is_land_biome(biome) if getattr(self.active_vehicle, 'land_vehicle', False)
                     else (biome is not None and not is_land_biome(biome))))
                if object_passable and biome_passable and vehicle_can_pass and not capsule_wall:
                    self.player_x, self.player_y = new_x, new_y
                else:
                    break
        player_move_x = self.player_x - old_player_x
        player_move_y = self.player_y - old_player_y
        self.player_mode = 'walking' if self._is_player_on_land() else 'swimming'
        self._update_whale_quest(old_player_x, old_player_y)
        if self.player_mode == 'walking' and abs(player_move_x) + abs(player_move_y) > 0.001:
            self.walk_animation += 1
        else:
            self.walk_animation = 0
        mouse_x, mouse_y = pygame.mouse.get_pos()
        for decoy in self.active_decoys:
            if decoy['in_cave'] != self.in_cave:
                continue
            screen_x = int((decoy['x'] - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            screen_y = int((decoy['y'] - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            pulse = 8 + self.frame_count // 6 % 6
            pygame.draw.circle(self.screen, (255, 170, 70), (screen_x, screen_y), pulse, 2)
            pygame.draw.circle(self.screen, (255, 235, 120), (screen_x, screen_y), 3)
        player_screen_x = VIEW_RADIUS * CELL_SIZE + CELL_SIZE // 2
        player_screen_y = VIEW_RADIUS * CELL_SIZE + CELL_SIZE // 2
        mouse_dx = mouse_x - player_screen_x
        mouse_dy = mouse_y - player_screen_y
        if mouse_dx != 0 or mouse_dy != 0:
            self.player_angle = math.degrees(math.atan2(mouse_dy, mouse_dx))
        self.mouse_world_x, self.mouse_world_y = self.screen_to_world(mouse_x, mouse_y)
        self._update_vehicle_torpedoes()
        self._update_mad_moss()
        self._update_golems()
        if self.frame_count % 180 == 0:
            self._map_biome_species_cache = None
        active_radius = VIEW_RADIUS + 2
        active_radius_squared = active_radius ** 2
        player_x = self.player_x
        player_y = self.player_y
        active_world = self.cave_world if self.in_cave else self.world
        light_sources = self._collect_light_sources()
        for animal in tuple(self.cooldown_animals):
            if animal.hp <= 0:
                self.cooldown_animals.discard(animal)
                continue
            if animal.attack_timer > 0:
                animal.attack_timer -= 1
            if animal.attack_timer <= 0:
                self.cooldown_animals.discard(animal)
        for animal in self.get_nearby_animals(active_radius):
            if animal.hp <= 0:
                continue
            if getattr(animal, 'stasis_timer', 0) > 0:
                animal.stasis_timer -= 1
                animal.active = True
                continue
            animal_dx = animal.x - player_x
            animal_dy = animal.y - player_y
            distance_squared = animal_dx ** 2 + animal_dy ** 2
            if distance_squared > active_radius_squared:
                animal.active = False
                continue
            # Full AI near the diver; lower frequency for distant, non-combat fauna.
            # Pets, pursuing enemies and large quest fauna never lose responsiveness.
            is_pet = getattr(animal, 'is_companion', False)
            far_passive = (distance_squared > 49.0 and not is_pet
                           and animal.type.behavior != 'predator'
                           and getattr(animal, 'pet_combat_target', None) is None)
            if far_passive and (self.frame_count + getattr(animal, 'world_order', 0)) % 4 != 0:
                continue
            old_animal_x = animal.x
            old_animal_y = animal.y
            if is_pet:
                self._update_companion(animal, player_move_x, player_move_y)
            else:
                pet_target = getattr(animal, 'pet_combat_target', None)
                pet_target_valid = pet_target is not None and getattr(pet_target, 'is_companion', False) and (pet_target.hp > 0) and (pet_target in self.get_current_animals()) and (abs(pet_target.x - player_x) <= VIEW_RADIUS) and (abs(pet_target.y - player_y) <= VIEW_RADIUS) and (animal.type.behavior == 'predator')
                if not pet_target_valid:
                    animal.pet_combat_target = None
                    pet_target = None
                if pet_target is not None:
                    animal.update(pet_target.x, pet_target.y, active_world)
                else:
                    animal.update(player_x, player_y, active_world)
            light_frightened = False
            if not getattr(animal, 'is_companion', False) and (getattr(animal, 'pet_combat_target', None) is None):
                light_frightened = self._apply_light_behavior(animal, active_world, light_sources)
            if not self.in_cave and (not getattr(animal, 'is_companion', False)) and self.is_start_capsule_wall(int(animal.x), int(animal.y)):
                animal.x = old_animal_x
                animal.y = old_animal_y
                animal.dx = -animal.dx
                animal.dy = -animal.dy
                animal._update_rotation()
            if not self.in_cave:
                self.world.update_animal_index(animal, old_animal_x, old_animal_y)
            if animal.type.behavior == 'predator' and (not getattr(animal, 'is_companion', False)):
                same_layer_decoys = [d for d in self.active_decoys if d['in_cave'] == self.in_cave]
                if same_layer_decoys:
                    nearest_decoy = min(same_layer_decoys, key=lambda d: (animal.x - d['x']) ** 2 + (animal.y - d['y']) ** 2)
                    ddx = nearest_decoy['x'] - animal.x
                    ddy = nearest_decoy['y'] - animal.y
                    dist = math.hypot(ddx, ddy)
                    if 0.1 < dist <= 6.0:
                        effect_old_x, effect_old_y = (animal.x, animal.y)
                        candidate_x = animal.x + ddx / dist * animal.type.speed * 1.6
                        candidate_y = animal.y + ddy / dist * animal.type.speed * 1.6
                        if animal.home_biome.contains(candidate_x, candidate_y):
                            animal.x = candidate_x
                            animal.y = candidate_y
                        animal.attack_timer = max(animal.attack_timer, 45)
                        animal._update_rotation()
                        if not self.in_cave:
                            self.world.update_animal_index(animal, effect_old_x, effect_old_y)
                if self.active_flare_timer > 0 and (not self._is_dark_night()) and (animal.type.name not in LIGHT_ATTRACTION):
                    flare_dx = animal.x - player_x
                    flare_dy = animal.y - player_y
                    flare_dist_sq = flare_dx ** 2 + flare_dy ** 2
                    if 0.01 < flare_dist_sq <= 49.0:
                        flare_dist = math.sqrt(flare_dist_sq)
                        effect_old_x, effect_old_y = (animal.x, animal.y)
                        candidate_x = animal.x + flare_dx / flare_dist * animal.type.speed * 2.2
                        candidate_y = animal.y + flare_dy / flare_dist * animal.type.speed * 2.2
                        if animal.home_biome.contains(candidate_x, candidate_y):
                            animal.x = candidate_x
                            animal.y = candidate_y
                        animal.attack_timer = max(animal.attack_timer, 45)
                        animal._update_rotation()
                        if not self.in_cave:
                            self.world.update_animal_index(animal, effect_old_x, effect_old_y)
            if getattr(animal, 'is_companion', False) or animal.type.behavior != 'predator' or (not animal.active) or light_frightened or (animal.type.name == WHALE_NAME):
                continue
            pet_target = getattr(animal, 'pet_combat_target', None)
            if pet_target is not None and getattr(pet_target, 'is_companion', False) and (pet_target.hp > 0) and (pet_target in self.get_current_animals()):
                attack_dx = animal.x - pet_target.x
                attack_dy = animal.y - pet_target.y
                attack_distance_squared = attack_dx ** 2 + attack_dy ** 2
                if attack_distance_squared <= 2.25 and animal.attack_timer <= 0:
                    pet_target.hp -= max(1, int(animal.type.damage))
                    animal.attack_timer = animal.attack_cooldown
                    self.cooldown_animals.add(animal)
                    if pet_target.hp <= 0:
                        pet_target.hp = 0
                        self._clear_companion_target(pet_target)
                        self.remove_current_animal(pet_target)
                        animal.pet_combat_target = None
                        self.notification = f'{pet_target.type.name} погиб в бою с {animal.type.name}'
                        self.notification_timer = 90
                continue
            attack_dx = animal.x - player_x
            attack_dy = animal.y - player_y
            attack_distance_squared = attack_dx ** 2 + attack_dy ** 2
            if attack_distance_squared <= 2.25 and animal.attack_timer <= 0:
                if self.active_vehicle is not None:
                    self.vehicle_take_damage(animal.type.damage)
                else:
                    self.player_take_damage(animal.type.damage, source=animal.type.name)
                animal.attack_timer = animal.attack_cooldown
                self.cooldown_animals.add(animal)
        self._update_special_creature_attacks()
        self.hunger_timer += 1
        self.thirst_timer += 1
        hunger_loss = 0.2
        thirst_loss = 0.2
        if self.inventory_counts.get(water_filtration_suit, 0) > 0:
            thirst_loss *= 0.7
        if self.inventory_counts.get(rebreather_type, 0) > 0:
            hunger_loss *= 0.85
        if self.hunger_timer >= 60:
            self.hunger_timer = 0
            self.player_hunger = max(0, self.player_hunger - hunger_loss)
            if self.player_hunger <= 0:
                self.player_take_damage(1)
        if self.thirst_timer >= 60:
            self.thirst_timer = 0
            self.player_thirst = max(0, self.player_thirst - thirst_loss)
            if self.player_thirst <= 0:
                self.player_take_damage(1)
        self._update_night_plankton()
        self._update_base_effects()
        self.update_notification()

    def _damage_player_or_vehicle(self, damage):
        if self.active_vehicle is not None:
            self.vehicle_take_damage(damage)
        else:
            self.player_take_damage(damage)

    def _add_special_explosion(self, x, y, kind, in_cave, radius=1.0, duration_ms=420):
        now = pygame.time.get_ticks()
        self.special_explosions.append({'x': float(x), 'y': float(y), 'kind': kind, 'in_cave': bool(in_cave), 'radius': float(radius), 'created_at': now, 'expires_at': now + int(duration_ms)})

    def _spawn_hostile_projectile(self, x, y, target_x, target_y, kind, speed, damage, in_cave, spread=0.0, target_animal=None, companion_owner=None):
        dx = target_x - x
        dy = target_y - y
        length = math.hypot(dx, dy)
        if length < 0.001:
            angle = random.uniform(0.0, math.tau)
        else:
            angle = math.atan2(dy, dx)
        angle += random.uniform(-spread, spread) if spread else 0.0
        vx = math.cos(angle) * speed
        vy = math.sin(angle) * speed
        self.hostile_projectiles.append({'x': float(x), 'y': float(y), 'vx': vx, 'vy': vy, 'damage': int(damage), 'ttl': 110 if kind == 'quest_wave' else 100 if kind == 'needle' else 90, 'in_cave': bool(in_cave), 'kind': kind, 'hit_radius': 0.7 if kind == 'quest_wave' else 0.3 if kind == 'needle' else 0.43, 'target_animal': target_animal, 'companion_owner': companion_owner})



    def _update_special_creature_attacks(self):
        """Шипы Tiger Plant, очереди Морского дракона, газовые шары и взрывы."""
        now = pygame.time.get_ticks()
        player_x, player_y = (self.player_x, self.player_y)
        in_cave = bool(self.in_cave)
        flora_grid = self._current_flora_grid()
        center_x, center_y = (int(player_x), int(player_y))
        for y in range(center_y - 5, center_y + 6):
            for x in range(center_x - 5, center_x + 6):
                flora = flora_grid.get((x, y))
                if flora is None or getattr(flora, 'kind', '') != 'tiger_plant':
                    continue
                dx, dy = (player_x - x, player_y - y)
                distance_sq = dx * dx + dy * dy
                if distance_sq < 0.15 or distance_sq > 25.0:
                    continue
                key = (in_cave, x, y)
                if now < self.plant_attack_timers.get(key, 0):
                    continue
                self._spawn_hostile_projectile(x, y, player_x, player_y, 'needle', speed=0.66, damage=4, in_cave=in_cave, spread=0.025)
                self.plant_attack_timers[key] = now + 1150
        for animal in self.get_nearby_animals(VIEW_RADIUS + 2):
            if animal.hp <= 0:
                continue
            is_companion = bool(getattr(animal, 'is_companion', False))
            target_animal = None
            companion_owner = None
            if animal.type.name in ('Морской дракон-левиафан', 'Морской дракон-левиафан'):
                if is_companion:
                    target_animal = getattr(animal, 'companion_target', None)
                    current_animals = self.get_current_animals()
                    if target_animal is None or target_animal not in current_animals or target_animal.hp <= 0 or getattr(target_animal, 'is_companion', False) or (abs(target_animal.x - player_x) > VIEW_RADIUS) or (abs(target_animal.y - player_y) > VIEW_RADIUS):
                        continue
                    companion_owner = animal
                    target_x, target_y = (target_animal.x, target_animal.y)
                    distance_sq = (target_x - animal.x) ** 2 + (target_y - animal.y) ** 2
                else:
                    target_x, target_y = (player_x, player_y)
                    dx, dy = (target_x - animal.x, target_y - animal.y)
                    distance_sq = dx * dx + dy * dy
                if distance_sq > 8.0 * 8.0 or distance_sq < 1.1 * 1.1:
                    continue
                if not hasattr(animal, 'fire_burst_remaining'):
                    animal.fire_burst_remaining = 0
                    animal.fire_burst_next_at = now + random.randint(450, 950)
                    animal.fire_next_attack_at = animal.fire_burst_next_at
                if animal.fire_burst_remaining > 0:
                    if now < animal.fire_burst_next_at:
                        continue
                    self._spawn_hostile_projectile(animal.x, animal.y, target_x, target_y, 'fireball', speed=0.44, damage=8, in_cave=in_cave, spread=0.065, target_animal=target_animal, companion_owner=companion_owner)
                    animal.fire_burst_remaining -= 1
                    if animal.fire_burst_remaining > 0:
                        animal.fire_burst_next_at = now + 190
                    else:
                        animal.fire_next_attack_at = now + 2100
                elif now >= animal.fire_next_attack_at:
                    animal.fire_burst_remaining = 3
                    self._spawn_hostile_projectile(animal.x, animal.y, target_x, target_y, 'fireball', speed=0.44, damage=8, in_cave=in_cave, spread=0.065, target_animal=target_animal, companion_owner=companion_owner)
                    animal.fire_burst_remaining = 2
                    animal.fire_burst_next_at = now + 190
            elif animal.type.name == WHALE_NAME:
                if is_companion:
                    continue
                dist_sq = (animal.x - player_x) ** 2 + (animal.y - player_y) ** 2
                if dist_sq > 9.0 ** 2 or dist_sq < 0.3 ** 2:
                    continue
                if now < getattr(animal, 'whale_wave_next_at', 0):
                    continue
                self._spawn_hostile_projectile(animal.x, animal.y, player_x, player_y, 'quest_wave', speed=0.28, damage=0, in_cave=in_cave, spread=0.025)
                animal.whale_wave_next_at = now + 4500
        projectile_survivors = []
        for projectile in self.hostile_projectiles:
            if projectile['in_cave'] != in_cave:
                projectile_survivors.append(projectile)
                continue
            old_x, old_y = (projectile['x'], projectile['y'])
            new_x = old_x + projectile['vx']
            new_y = old_y + projectile['vy']
            projectile['ttl'] -= 1
            if projectile['ttl'] <= 0 or not (0 <= new_x < MAP_SIZE and 0 <= new_y < MAP_SIZE):
                continue
            if in_cave and (not self.cave_is_passable(new_x, new_y)):
                continue
            sx, sy = (projectile['vx'], projectile['vy'])
            seg_len_sq = sx * sx + sy * sy
            target_animal = projectile.get('target_animal')
            if target_animal is not None:
                if target_animal.hp <= 0 or target_animal not in self.get_current_animals() or getattr(target_animal, 'is_companion', False):
                    continue
                target_x, target_y = (target_animal.x, target_animal.y)
                if seg_len_sq > 1e-09:
                    t = max(0.0, min(1.0, ((target_x - old_x) * sx + (target_y - old_y) * sy) / seg_len_sq))
                else:
                    t = 0.0
                closest_x = old_x + sx * t
                closest_y = old_y + sy * t
                hit_dx = target_x - closest_x
                hit_dy = target_y - closest_y
                if hit_dx * hit_dx + hit_dy * hit_dy <= (projectile['hit_radius'] + 0.35) ** 2:
                    target_animal.hp -= projectile['damage']
                    owner = projectile.get('companion_owner')
                    if target_animal.hp <= 0:
                        target_animal.hp = 0
                        if owner is not None and owner.hp > 0:
                            self._finish_companion_kill(owner, target_animal)
                        else:
                            self.remove_current_animal(target_animal)
                    if projectile['kind'] == 'fireball':
                        self._add_special_explosion(new_x, new_y, 'fireball', in_cave, 0.65, 260)
                    continue
            else:
                if seg_len_sq > 1e-09:
                    t = max(0.0, min(1.0, ((player_x - old_x) * sx + (player_y - old_y) * sy) / seg_len_sq))
                else:
                    t = 0.0
                closest_x = old_x + sx * t
                closest_y = old_y + sy * t
                hit_dx = player_x - closest_x
                hit_dy = player_y - closest_y
                if hit_dx * hit_dx + hit_dy * hit_dy <= projectile['hit_radius'] ** 2:
                    if projectile['kind'] == 'quest_wave':
                        self._start_whale_quest()
                    else:
                        self._damage_player_or_vehicle(projectile['damage'])
                    if projectile['kind'] == 'fireball':
                        self._add_special_explosion(new_x, new_y, 'fireball', in_cave, 0.65, 260)
                    continue
            projectile['x'], projectile['y'] = (new_x, new_y)
            projectile_survivors.append(projectile)
        self.hostile_projectiles = projectile_survivors
        self.special_explosions = [effect for effect in self.special_explosions if now < effect['expires_at']]

    def update_notification(self):
        if self.notification_timer > 0:
            self.notification_timer -= 1
            if self.notification_timer == 0:
                self.notification = ''

    def draw_dropped_item_at(self, x, y, screen_x, screen_y):
        dropped = self._current_dropped_items().get((x, y))
        if not dropped:
            return
        item = dropped[-1]
        center = (screen_x + CELL_SIZE // 2, screen_y + CELL_SIZE // 2)
        if item.image:
            image = self.get_inventory_image(item)
            rect = image.get_rect(center=center)
            self.screen.blit(image, rect)
        elif item.shape == 'circle':
            pygame.draw.circle(self.screen, item.color, center, min(item.size // 2, CELL_SIZE // 2 - 3))
        else:
            size = min(item.size, CELL_SIZE - 6)
            rect = pygame.Rect(0, 0, size, size)
            rect.center = center
            pygame.draw.rect(self.screen, item.color, rect)
        if len(dropped) > 1:
            count_text = self.tiny_font.render(str(len(dropped)), True, (255, 255, 255))
            self.screen.blit(count_text, (screen_x + CELL_SIZE - count_text.get_width() - 2, screen_y + CELL_SIZE - count_text.get_height() - 1))

    def render(self):
        if self.state == 'map':
            self.draw_map()
            self.draw_damage_vignette()
            pygame.display.flip()
            return
        world_zoom = self.camera_zoom
        self._render_view_radius = max(1, math.ceil((SCREEN_SIZE / (CELL_SIZE * world_zoom) - 1) / 2))
        self._render_canvas_size = (self._render_view_radius * 2 + 1) * CELL_SIZE
        display_surface = pygame.display.get_surface()
        if abs(world_zoom - 1.0) < 1e-08:
            self.screen = display_surface
        else:
            need_size = (self._render_canvas_size, self._render_canvas_size)
            if self._world_render_surface is None or self._world_render_surface.get_size() != need_size:
                self._world_render_surface = pygame.Surface(need_size).convert()
            self.screen = self._world_render_surface
        if self.lighting_overlay.get_size() != self.screen.get_size():
            self.lighting_overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA).convert_alpha()
        self.screen.fill((0, 0, 0))
        camera_x, camera_y = self.get_render_camera_origin()
        start_x = math.floor(camera_x)
        start_y = math.floor(camera_y)
        end_x = math.ceil(camera_x + self._render_view_radius * 2)
        end_y = math.ceil(camera_y + self._render_view_radius * 2)
        for y in range(start_y, end_y + 1):
            if not 0 <= y < MAP_SIZE:
                continue
            screen_y = int(round((y - camera_y) * CELL_SIZE))
            for x in range(start_x, end_x + 1):
                if not 0 <= x < MAP_SIZE:
                    continue
                screen_x = int(round((x - camera_x) * CELL_SIZE))
                if screen_x >= self.screen.get_width() or screen_x + CELL_SIZE <= 0 or screen_y >= self.screen.get_height() or screen_y + CELL_SIZE <= 0:
                    continue
                if self.in_cave:
                    tunnel = self.get_cave_tunnel_at(x, y)
                    self.draw_cave_cell(x, y, screen_x, screen_y, tunnel)
                    flora = self.cave_flora_grid.get((x, y))
                    if flora is not None and flora.image is not None:
                        flora_rect = flora.image.get_rect(center=(screen_x + CELL_SIZE // 2, screen_y + CELL_SIZE // 2))
                        self.screen.blit(flora.image, flora_rect)
                    obj = self.cave_object_grid.get((x, y))
                    if obj is not None and (not getattr(obj, 'is_buildable', False)):
                        center_x = screen_x + CELL_SIZE // 2
                        center_y = screen_y + CELL_SIZE // 2
                        if obj.image:
                            rect = obj.image.get_rect(center=(center_x, center_y))
                            self.screen.blit(obj.image, rect)
                        elif obj.shape == 'circle':
                            pygame.draw.circle(self.screen, obj.color, (center_x, center_y), obj.size // 2)
                        else:
                            rect = pygame.Rect(0, 0, obj.size, obj.size)
                            rect.center = (center_x, center_y)
                            pygame.draw.rect(self.screen, obj.color, rect)
                    exit_data = self.get_cave_exit_at(x, y)
                    if exit_data is not None:
                        pygame.draw.rect(self.screen, (16, 74, 100), (screen_x, screen_y, CELL_SIZE, CELL_SIZE))
                        pygame.draw.rect(self.screen, (175, 230, 255), (screen_x, screen_y, CELL_SIZE, CELL_SIZE), 2)
                    self.draw_dropped_item_at(x, y, screen_x, screen_y)
                    if (x, y) in self.scanner_highlight_cells:
                        pygame.draw.rect(self.screen, (70, 250, 170), (screen_x + 1, screen_y + 1, CELL_SIZE - 2, CELL_SIZE - 2), 2)
                    continue
                biome = self.world.get_biome_at(x, y)
                color = biome.color if biome is not None else (3, 7, 19)
                if self.is_aurora_biome(biome):
                    self.draw_aurora_cell(x, y, screen_x, screen_y, biome)
                else:
                    pygame.draw.rect(self.screen, color, (screen_x, screen_y, CELL_SIZE, CELL_SIZE))
                    if biome is None:
                        seed = self._stable_visual_hash('ring_star', x, y)
                        if seed % 6 == 0:
                            star_color = ((55, 100, 158), (105, 150, 211), (180, 204, 238))[seed % 3]
                            star_x = screen_x + 4 + seed // 17 % (CELL_SIZE - 8)
                            star_y = screen_y + 4 + seed // 71 % (CELL_SIZE - 8)
                            pygame.draw.circle(self.screen, star_color, (star_x, star_y), 1)
                    else:
                        self.draw_surface_seabed_cell(x, y, screen_x, screen_y, biome)
                    self.draw_start_capsule_cell(x, y, screen_x, screen_y)
                self.draw_land_tree(x, y, screen_x, screen_y)
                flora = self.world.flora_grid.get((x, y))
                if flora is not None and flora.image is not None:
                    flora_rect = flora.image.get_rect(center=(screen_x + CELL_SIZE // 2, screen_y + CELL_SIZE // 2))
                    self.screen.blit(flora.image, flora_rect)
                obj = self.world.object_grid.get((x, y))
                if obj is not None and (not getattr(obj, 'is_buildable', False)):
                    center_x = screen_x + CELL_SIZE // 2
                    center_y = screen_y + CELL_SIZE // 2
                    if obj.image:
                        rect = obj.image.get_rect(center=(center_x, center_y))
                        self.screen.blit(obj.image, rect)
                    elif obj.shape == 'circle':
                        pygame.draw.circle(self.screen, obj.color, (center_x, center_y), obj.size // 2)
                    else:
                        rect = pygame.Rect(0, 0, obj.size, obj.size)
                        rect.center = (center_x, center_y)
                        pygame.draw.rect(self.screen, obj.color, rect)
                entrance = self.get_surface_cave_entrance_at(x, y)
                if entrance is not None:
                    pygame.draw.rect(self.screen, (8, 8, 12), (screen_x, screen_y, CELL_SIZE, CELL_SIZE))
                    pygame.draw.rect(self.screen, (40, 45, 55), (screen_x, screen_y, CELL_SIZE, CELL_SIZE), 1)
                self.draw_dropped_item_at(x, y, screen_x, screen_y)
                if (x, y) in self.scanner_highlight_cells:
                    pygame.draw.rect(self.screen, (70, 250, 170), (screen_x + 1, screen_y + 1, CELL_SIZE - 2, CELL_SIZE - 2), 2)
        foreground_objects = self._get_current_object_grid()
        foreground_planters = self._planter_types()
        for y in range(start_y, end_y + 1):
            if not 0 <= y < MAP_SIZE:
                continue
            for x in range(start_x, end_x + 1):
                if not 0 <= x < MAP_SIZE:
                    continue
                screen_x = int(round((x - camera_x) * CELL_SIZE))
                screen_y = int(round((y - camera_y) * CELL_SIZE))
                if not self.in_cave and self.is_inside_start_capsule(x, y):
                    self.draw_start_capsule_cell(x, y, screen_x, screen_y)
                obj = foreground_objects.get((x, y))
                if obj is None or not getattr(obj, 'is_buildable', False):
                    continue
                center = (screen_x + CELL_SIZE // 2, screen_y + CELL_SIZE // 2)
                if obj.image is not None:
                    self.screen.blit(obj.image, obj.image.get_rect(center=center))
                elif obj.shape == 'circle':
                    pygame.draw.circle(self.screen, obj.color, center, max(1, obj.size // 2))
                else:
                    pygame.draw.rect(self.screen, obj.color, pygame.Rect(0, 0, obj.size, obj.size).move(center[0] - obj.size // 2, center[1] - obj.size // 2))
                if obj in foreground_planters:
                    self._draw_planted_flora_at(x, y, screen_x, screen_y)
        player_x = self.player_x
        player_y = self.player_y
        if self.setting_workers_enabled:
            self.foreman.draw(self.screen, camera_x, camera_y)
        self._draw_mad_moss(camera_x, camera_y)
        self._draw_golems(camera_x, camera_y)
        self.draw_vehicle_wrecks(camera_x, camera_y)
        self.draw_vehicle_torpedoes(camera_x, camera_y)
        for animal in self.get_nearby_animals(self._render_view_radius):
            if animal.hp <= 0:
                continue
            screen_x = int((animal.x - camera_x) * CELL_SIZE + CELL_SIZE // 2)
            screen_y = int((animal.y - camera_y) * CELL_SIZE + CELL_SIZE // 2)
            animal_type = animal.type
            if animal_type.image:
                image = animal.rotated_image or animal_type.image
                rect = image.get_rect(center=(screen_x, screen_y))
                self.screen.blit(image, rect)
            elif animal_type.shape == 'circle':
                pygame.draw.circle(self.screen, animal_type.color, (screen_x, screen_y), animal_type.size // 2)
            else:
                rect = pygame.Rect(0, 0, animal_type.size, animal_type.size)
                rect.center = (screen_x, screen_y)
                pygame.draw.rect(self.screen, animal_type.color, rect)
            if getattr(animal, 'is_companion', False):
                marker_radius = max(9, min(28, animal_type.size // 2 + 6))
                pygame.draw.circle(self.screen, (80, 245, 225), (screen_x, screen_y), marker_radius, 2)
            if 0 < animal.hp < animal.max_hp:
                bar_width = 20
                bar_height = 4
                bar_x = screen_x - bar_width // 2
                bar_y = screen_y - animal_type.size // 2 - 8
                pygame.draw.rect(self.screen, (120, 0, 0), (bar_x, bar_y, bar_width, bar_height))
                fill = int(bar_width * animal.hp / animal.max_hp)
                pygame.draw.rect(self.screen, (0, 220, 0), (bar_x, bar_y, fill, bar_height))
        self._draw_hostile_attacks(camera_x, camera_y)
        player_screen_x = self._render_view_radius * CELL_SIZE + CELL_SIZE // 2
        player_screen_y = self._render_view_radius * CELL_SIZE + CELL_SIZE // 2
        if self.active_vehicle is not None:
            vehicle_image = self.get_vehicle_rotated(self.active_vehicle, self.player_angle)
            if vehicle_image is not None:
                vehicle_rect = vehicle_image.get_rect(center=(player_screen_x, player_screen_y))
                self.screen.blit(vehicle_image, vehicle_rect)
        else:
            if self.equipped_weapon is not None:
                weapon_image = self.get_weapon_rotated(self.equipped_weapon, self.player_angle)
                if weapon_image:
                    radians = math.radians(self.player_angle)
                    offset = CELL_SIZE * 0.55
                    weapon_x = player_screen_x + math.cos(radians) * offset
                    weapon_y = player_screen_y + math.sin(radians) * offset
                    rect = weapon_image.get_rect(center=(int(weapon_x), int(weapon_y)))
                    self.screen.blit(weapon_image, rect)
            diver_image = self.get_diver_rotated(self.player_angle)
            diver_rect = diver_image.get_rect(center=(player_screen_x, player_screen_y))
            self.screen.blit(diver_image, diver_rect)
            pygame.draw.circle(self.screen, (255, 255, 255), (player_screen_x, player_screen_y), CELL_SIZE // 2 + 2, 1)
        self.draw_day_night_overlay()
        self.draw_light_emitters(camera_x, camera_y)
        self.draw_bioluminescence(camera_x, camera_y)
        if self.screen is not display_surface:
            target_px = max(1, round(self._render_canvas_size * world_zoom))
            scaled_world = pygame.transform.smoothscale(self.screen, (target_px, target_px))
            self.screen = display_surface
            display_surface.fill((0, 0, 0))
            screen_offset = (SCREEN_SIZE - target_px) // 2
            self.screen.blit(scaled_world, (screen_offset, screen_offset))
        if world_zoom > ZOOM_DETAIL_START and self.screen is display_surface:
            self._draw_zoom_details(world_zoom, target_px, screen_offset)
        self.draw_night_plankton()
        self._draw_blue_edge_haze()
        self._draw_night_edge_vignette()
        self.draw_hud()
        self._draw_whale_quest_hud()
        if self.state == 'inventory':
            self.draw_inventory()
        elif self.state == 'crafting':
            self.draw_crafting()
        elif self.state == 'pets':
            self.draw_pet_list()
        elif self.state == 'scanner':
            self.draw_scanner_view()
        elif self.state == 'planting':
            self.draw_planting_menu()
        if self.settings_open:
            self.draw_settings_panel()
        if self.state == 'crew_panel' and self.setting_workers_enabled:
            self.foreman.panel()
        self._draw_whale_quest_popup()
        if self.notification_timer > 0 and self.notification and (not self.settings_open):
            text = self.large_font.render(self.notification, True, (255, 255, 0))
            rect = text.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE // 2 + 100))
            pygame.draw.rect(self.screen, (0, 0, 0), rect.inflate(20, 10))
            self.screen.blit(text, rect)
        if self.state == 'playing':
            mouse_x, mouse_y = pygame.mouse.get_pos()
            pygame.draw.line(self.screen, (255, 255, 255), (mouse_x - 8, mouse_y), (mouse_x + 8, mouse_y), 2)
            pygame.draw.line(self.screen, (255, 255, 255), (mouse_x, mouse_y - 8), (mouse_x, mouse_y + 8), 2)
            pygame.draw.circle(self.screen, (255, 255, 255), (mouse_x, mouse_y), 3, 1)
        self.draw_damage_vignette()
        pygame.display.flip()

    def _create_damage_vignette_surface(self):
        """Prebuild a red edge gradient whose centre stays fully transparent."""
        vignette = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA)
        fade_width = max(90, int(SCREEN_SIZE * 0.34))
        for inset in range(fade_width):
            t = 1.0 - inset / max(1, fade_width - 1)
            alpha = int(235 * t ** 2.15)
            if alpha <= 0:
                continue
            w = SCREEN_SIZE - inset * 2
            h = SCREEN_SIZE - inset * 2
            if w <= 0 or h <= 0:
                break
            pygame.draw.rect(vignette, (235, 8, 8, alpha), pygame.Rect(inset, inset, w, h), 1)
        return vignette.convert_alpha()

    def draw_damage_vignette(self):
        """Tint screen edges red according to lost HP, with a brief boost on each hit."""
        if self.max_player_hp <= 0:
            return
        missing_ratio = 1.0 - max(0.0, min(1.0, self.player_hp / self.max_player_hp))
        if missing_ratio <= 0.0 and self.damage_vignette_flash <= 0:
            return
        if self.damage_vignette_surface is None:
            self.damage_vignette_surface = self._create_damage_vignette_surface()
        persistent_alpha = 0
        if missing_ratio > 0.0:
            persistent_alpha = int(38 + 182 * missing_ratio ** 0.78)
        flash_alpha = 0
        if self.damage_vignette_flash > 0:
            flash_t = self.damage_vignette_flash / 18.0
            flash_alpha = int(255 * flash_t ** 0.55)
            self.damage_vignette_flash -= 1
        alpha = max(persistent_alpha, flash_alpha)
        self.damage_vignette_surface.set_alpha(max(0, min(255, alpha)))
        self.screen.blit(self.damage_vignette_surface, (0, 0))
        self.damage_vignette_surface.set_alpha(None)

    def _settings_option_rects(self):
        """Единая геометрия для отображения настроек и обработки мыши."""
        panel = pygame.Rect((SCREEN_SIZE - 520) // 2, (SCREEN_SIZE - 400) // 2, 520, 400)
        return [pygame.Rect(panel.x + 24, panel.y + 84 + index * 58, 472, 48)
                for index in range(4)]

    def draw_settings_panel(self):
        """Модальная панель настроек, включая одиночное прохождение без NPC."""
        overlay = pygame.Surface((SCREEN_SIZE, SCREEN_SIZE), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 175))
        self.screen.blit(overlay, (0, 0))
        panel = pygame.Rect((SCREEN_SIZE - 520) // 2, (SCREEN_SIZE - 400) // 2, 520, 400)
        pygame.draw.rect(self.screen, (13, 32, 47), panel, border_radius=12)
        pygame.draw.rect(self.screen, (74, 174, 205), panel, 2, border_radius=12)
        title = self.large_font.render('Настройки', True, (225, 247, 255))
        self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 36)))
        subtitle = self.small_font.render('N — закрыть панель', True, (155, 190, 205))
        self.screen.blit(subtitle, subtitle.get_rect(center=(panel.centerx, panel.y + 62)))
        options = [
            ('Отключить планктон', not self.setting_plankton_enabled),
            ('Отключить мелкие объекты на дне', not self.setting_seabed_details_enabled),
            ('Отключить затемнение воды', not self.setting_water_darkening_enabled),
            ('Отключить всех рабочих', not self.setting_workers_enabled),
        ]
        for rect, (label, disabled) in zip(self._settings_option_rects(), options):
            pygame.draw.rect(self.screen, (25, 55, 72), rect, border_radius=7)
            pygame.draw.rect(self.screen, (69, 112, 132), rect, 1, border_radius=7)
            check = pygame.Rect(rect.x + 14, rect.y + 12, 24, 24)
            pygame.draw.rect(self.screen, (8, 20, 29), check, border_radius=4)
            pygame.draw.rect(self.screen, (124, 205, 221), check, 2, border_radius=4)
            if disabled:
                pygame.draw.line(self.screen, (255, 190, 105), (check.x + 5, check.y + 12), (check.x + 10, check.y + 18), 3)
                pygame.draw.line(self.screen, (255, 190, 105), (check.x + 10, check.y + 18), (check.x + 20, check.y + 5), 3)
            text = self.font.render(label, True, (230, 241, 247))
            self.screen.blit(text, text.get_rect(midleft=(rect.x + 52, rect.centery)))
        hint = self.tiny_font.render('Отключение рабочих — одиночный режим. Прогресс бригады не теряется.',
                                     True, (153, 183, 195))
        self.screen.blit(hint, hint.get_rect(center=(panel.centerx, panel.bottom - 16)))

    def draw_hud(self):
        hp_text = self.font.render(f'HP: {self.player_hp}/{self.max_player_hp}', True, (255, 255, 255))
        self.screen.blit(hp_text, (10, 10))
        bar_width = 100
        bar_height = 12
        bar_x = 10
        pygame.draw.rect(self.screen, (100, 0, 0), (bar_x, 35, bar_width, bar_height))
        fill = int(bar_width * self.player_hp / self.max_player_hp)
        pygame.draw.rect(self.screen, (0, 200, 0), (bar_x, 35, fill, bar_height))
        hunger_text = self.font.render(f'Голод: {int(self.player_hunger)}', True, (255, 255, 255))
        self.screen.blit(hunger_text, (10, 55))
        pygame.draw.rect(self.screen, (50, 50, 50), (bar_x, 75, bar_width, bar_height))
        fill = int(bar_width * self.player_hunger / self.max_hunger)
        pygame.draw.rect(self.screen, (0, 255, 0), (bar_x, 75, fill, bar_height))
        thirst_text = self.font.render(f'Жажда: {int(self.player_thirst)}', True, (255, 255, 255))
        self.screen.blit(thirst_text, (10, 95))
        pygame.draw.rect(self.screen, (50, 50, 50), (bar_x, 115, bar_width, bar_height))
        fill = int(bar_width * self.player_thirst / self.max_thirst)
        pygame.draw.rect(self.screen, (0, 100, 255), (bar_x, 115, fill, bar_height))
        if self.active_vehicle is not None:
            vehicle_hp = self.vehicle_hp.get(self.active_vehicle, 0)
            vehicle_max = self.vehicle_max_hp[self.active_vehicle]
            text = self.font.render(f"{self.active_vehicle.name} HP: {vehicle_hp}/{vehicle_max} | Фары L: {('ВКЛ' if self.vehicle_headlights_on else 'ВЫКЛ')}", True, (120, 225, 245))
            self.screen.blit(text, (10, 140))
            pygame.draw.rect(self.screen, (75, 30, 30), (10, 165, 130, 10))
            vehicle_fill = int(130 * vehicle_hp / max(1, vehicle_max))
            pygame.draw.rect(self.screen, (45, 190, 215), (10, 165, vehicle_fill, 10))
        elif self.equipped_weapon is not None:
            text = self.font.render(f'Оружие: {self.equipped_weapon.name}', True, (230, 220, 80))
            self.screen.blit(text, (10, 140))
            stats = self.small_font.render(f'Урон: {self.equipped_weapon.damage} | Дальность: {self.equipped_weapon.weapon_range}', True, (210, 210, 210))
            self.screen.blit(stats, (10, 165))
        layer_text = self.small_font.render('Слой: пещера' if self.in_cave else 'Слой: поверхность', True, (180, 220, 240))
        self.screen.blit(layer_text, (10, 190))
        locomotion_text = self.small_font.render('Передвижение: ХОДЬБА' if self._is_player_on_land() else 'Передвижение: ПЛАВАНИЕ', True, (231, 215, 157) if self._is_player_on_land() else (125, 226, 252))
        self.screen.blit(locomotion_text, (10, 210))
        zoom_text = self.tiny_font.render(f'Камера: {self.camera_zoom:.2f}x  |  Колесо мыши: +/-', True, (160, 220, 245))
        self.screen.blit(zoom_text, (SCREEN_SIZE - zoom_text.get_width() - 10, 35))
        time_text = self.small_font.render(self.get_time_string(), True, (235, 235, 250))
        time_rect = time_text.get_rect()
        time_rect.topright = (SCREEN_SIZE - 10, 10)
        self.screen.blit(time_text, time_rect)
        statuses = []
        if self.flashlight_on:
            statuses.append('Фонарик')
        if self.seaglide_on:
            statuses.append('Глайдер')
        if self.active_flare_timer > 0:
            statuses.append('Ракета')
        if self.damage_resist_timer > 0:
            statuses.append('Защита')
        if self.speed_boost_timer > 0:
            statuses.append('Рывок')
        if statuses:
            status_text = self.small_font.render('Активно: ' + ', '.join(statuses), True, (255, 220, 120))
            self.screen.blit(status_text, (10, 215))
        hint = self.small_font.render('C — вход/всплыть | M — карта | R — инвентарь | F — транспорт | T — питомцы | I — крафт | U — использовать | ЛКМ — торпеда в судне | ПКМ — сканер/посадка', True, (210, 210, 210))
        self.screen.blit(hint, (10, SCREEN_SIZE - 25))

    def draw_pet_list(self):
        self.screen.blit(self.inventory_overlay, (0, 0))
        pets = self._get_all_companions()
        total = len(pets)
        max_page = max(0, (total - 1) // self.pets_per_page)
        self.pet_list_page = min(self.pet_list_page, max_page)
        if self.selected_pet not in pets:
            self.selected_pet = None
        title = self.large_font.render(f'Питомцы: {total}', True, (120, 245, 225))
        self.screen.blit(title, title.get_rect(center=(SCREEN_SIZE // 2, 47)))
        for i, instruction in enumerate(('1–9 — выбрать | E — вернуть в инвентарь | U — лечить (+35 HP/аптечка)', 'T или ESC — закрыть | ←/→ — страницы')):
            subtitle = self.small_font.render(instruction, True, (215, 225, 230))
            self.screen.blit(subtitle, subtitle.get_rect(center=(SCREEN_SIZE // 2, 77 + i * 24)))
        if not pets:
            empty = self.small_font.render('Нет питомцев. Выберите существо в инвентаре и нажмите H.', True, (225, 225, 225))
            self.screen.blit(empty, empty.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE // 2)))
            return
        start = self.pet_list_page * self.pets_per_page
        page_pets = pets[start:start + self.pets_per_page]
        y = 130
        row_height = 47
        for number, pet in enumerate(page_pets, start=1):
            selected = pet is self.selected_pet
            row_rect = pygame.Rect(14, y - 3, SCREEN_SIZE - 28, row_height - 3)
            pygame.draw.rect(self.screen, (24, 85, 76) if selected else (14, 35, 43), row_rect, border_radius=6)
            if selected:
                pygame.draw.rect(self.screen, (95, 255, 199), row_rect, width=2, border_radius=6)
            layer = 'пещера' if pet in self.cave_animals else 'поверхность'
            visible_here = pet in self.get_current_animals() and abs(pet.x - self.player_x) <= VIEW_RADIUS and (abs(pet.y - self.player_y) <= VIEW_RADIUS)
            state_text = getattr(pet, 'companion_state', 'следует за игроком')
            if not visible_here and pet in self.get_current_animals():
                state_text = 'ждёт вне видимости'
            follow_distance = self._companion_follow_distance(pet)
            hp_text = f'HP {max(0, int(pet.hp))}/{int(pet.max_hp)}'
            text_color = (170, 255, 204) if selected else (235, 240, 245)
            line_1 = self.small_font.render(f'{number}. {pet.type.name}  |  {hp_text}', True, text_color)
            line_2 = self.tiny_font.render(f'{layer}  |  дистанция {follow_distance:.1f}  |  {state_text}', True, (177, 207, 216))
            self.screen.blit(line_1, (24, y))
            self.screen.blit(line_2, (42, y + 24))
            y += row_height
        page_text = self.small_font.render(f'Страница {self.pet_list_page + 1}/{max_page + 1}', True, (180, 210, 220))
        self.screen.blit(page_text, page_text.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE - 33)))

    def draw_inventory(self):
        self.screen.blit(self.inventory_overlay, (0, 0))
        start_x, start_y, total_width, total_height = self._inventory_grid_layout()
        title = self.small_font.render('Инвентарь (R/ESC — закрыть)', True, (255, 255, 255))
        title_rect = title.get_rect(center=(start_x + total_width // 2, start_y - 30))
        self.screen.blit(title, title_rect)
        used = sum((1 for slot in self.inventory if slot is not None))
        counter = self.font.render(f'{used} / {INVENTORY_SIZE}', True, (200, 200, 200))
        counter_rect = counter.get_rect(center=(start_x + total_width // 2, start_y + total_height + 20))
        self.screen.blit(counter, counter_rect)
        if self.active_slot_index != -1:
            hint = self.small_font.render('U — использовать  |  P — выбросить', True, (210, 210, 210))
            hint_rect = hint.get_rect(center=(start_x + total_width // 2, start_y + total_height + 45))
            self.screen.blit(hint, hint_rect)
            companion_hint = self.small_font.render('H — питомец  |  F — транспорт  |  Q — оружие', True, (120, 235, 225))
            companion_hint_rect = companion_hint.get_rect(center=(start_x + total_width // 2, start_y + total_height + 68))
            self.screen.blit(companion_hint, companion_hint_rect)
            eat_hint = self.small_font.render('ПКМ — съесть рыбу  |  B — построить', True, (255, 215, 125))
            eat_hint_rect = eat_hint.get_rect(center=(start_x + total_width // 2, start_y + total_height + 91))
            self.screen.blit(eat_hint, eat_hint_rect)
        for row in range(INVENTORY_ROWS):
            for col in range(INVENTORY_COLS):
                index = row * INVENTORY_COLS + col
                x = start_x + col * (SLOT_SIZE + SLOT_MARGIN)
                y = start_y + row * (SLOT_SIZE + SLOT_MARGIN)
                pygame.draw.rect(self.screen, (55, 55, 65), (x, y, SLOT_SIZE, SLOT_SIZE))
                border_color = (0, 255, 0) if index == self.active_slot_index else (110, 110, 120)
                pygame.draw.rect(self.screen, border_color, (x, y, SLOT_SIZE, SLOT_SIZE), 3)
                item = self.inventory[index]
                if item is None:
                    continue
                center = (x + SLOT_SIZE // 2, y + SLOT_SIZE // 2)
                if item.image:
                    image = self.get_inventory_image(item)
                    rect = image.get_rect(center=center)
                    self.screen.blit(image, rect)
                elif item.shape == 'circle':
                    pygame.draw.circle(self.screen, item.color, center, min(item.size // 2, SLOT_SIZE // 2 - 5))
                else:
                    size = min(item.size, SLOT_SIZE - 8)
                    rect = pygame.Rect(0, 0, size, size)
                    rect.center = center
                    pygame.draw.rect(self.screen, item.color, rect)
                if item.is_weapon:
                    weapon_mark = self.small_font.render('W', True, (255, 230, 0))
                    self.screen.blit(weapon_mark, (x + 3, y + 2))
        self._draw_whale_quest_inventory_column()

    def draw_crafting(self):
        self.screen.blit(self.crafting_overlay, (0, 0))
        title = self.large_font.render('Крафт', True, (255, 255, 255))
        self.screen.blit(title, title.get_rect(center=(SCREEN_SIZE // 2, 30)))
        location_name = self.get_crafting_location_name()
        location_text = self.tiny_font.render(f"Зона крафта: {location_name or 'недоступна'}", True, (120, 230, 245))
        self.screen.blit(location_text, location_text.get_rect(center=(SCREEN_SIZE // 2, 55)))
        station_recipes = self.get_current_station_recipes()
        self.max_craft_page = max(0, (len(station_recipes) - 1) // self.recipes_per_page)
        self.craft_page = min(self.craft_page, self.max_craft_page)
        start = self.craft_page * self.recipes_per_page
        end = min(start + self.recipes_per_page, len(station_recipes))
        current_recipes = station_recipes[start:end]
        y = 82
        for index, recipe in enumerate(current_recipes):
            can_craft = recipe.can_craft(self.inventory_counts)
            color = (80, 255, 100) if can_craft else (255, 100, 100)
            output_suffix = f' x{recipe.result_count}' if recipe.result_count > 1 else ''
            title_text = self.small_font.render(f'{index + 1}. {recipe.name}{output_suffix}', True, color)
            self.screen.blit(title_text, (40, y))
            ingredients = ', '.join((f'{item.name} x{count}' for item, count in recipe.ingredients.items()))
            meta = f'[{recipe.station} / {recipe.category}] ' + ingredients
            ingredient_text = self.tiny_font.render(meta, True, (210, 210, 210))
            self.screen.blit(ingredient_text, (55, y + 21))
            y += 52
        page_text = self.small_font.render(f'Страница {self.craft_page + 1}/{self.max_craft_page + 1} | ← → | TAB: станция', True, (220, 220, 220))
        self.screen.blit(page_text, page_text.get_rect(center=(SCREEN_SIZE // 2, SCREEN_SIZE - 30)))

    def run(self):
        while self.running:
            self.handle_events()
            self.update()
            self.render()
            self.clock.tick(60)
        pygame.quit()
        sys.exit()


# >>> FASTENER_FISH_MOD_V1 >>>
# -*- coding: utf-8 -*-
"""МегоСубнатика2D: семейство рыб-крепежей.

Этот файл предназначен для встраивания установщиком
install_fastener_fish.py в исходный однофайловый скрипт игры.
Для интеграции используется только публичная структура версии
МегоСубнатика2D_големогенные_бактерии (19.10.2026).
"""


def install_fastener_fish(game_ns):
    """Подключить виды, рецепты и механику к основному модулю игры."""
    import math
    import random
    import pygame

    required = ('Game', 'Animal', 'AnimalType', 'ObjectType', 'Recipe',
                'init_assets', 'biomes')
    # biomes появляется при выполнении init_assets(), поэтому проверяем
    # только имена типов и методов, существующие во время импорта.
    missing = [name for name in required if name != 'biomes' and name not in game_ns]
    if missing:
        raise RuntimeError('Мод рыб-крепежей: неподдерживаемая версия игры: '
                           + ', '.join(missing))
    if game_ns.get('_FASTENER_FISH_INSTALLED'):
        return
    game_ns['_FASTENER_FISH_INSTALLED'] = True

    AnimalType = game_ns['AnimalType']
    ObjectType = game_ns['ObjectType']
    Recipe = game_ns['Recipe']
    Animal = game_ns['Animal']
    Game = game_ns['Game']
    rand = random.Random(20261020)
    KINDS = ('selftap', 'screw', 'nail', 'driver')
    BIOMES = {
        'Мелководье': (5, 2, 6, 1),
        'Глубоководье': (3, 4, 1, 2),
        'Центральное море': (4, 2, 3, 2),
        'Коралловый риф': (10, 5, 3, 3),
        'Кельповый лес': (10, 8, 4, 3),
        'Био-люминесцентная зона': (3, 4, 3, 4),
        'Озёра (внешние)': (3, 2, 4, 1),
        'Реки': (2, 1, 3, 1),
        'Притоки': (1, 0, 1, 0),
        'Водопады': (0, 0, 1, 0),
    }
    FISH_INFO = {
        'selftap': ('Рыба-саморез', (158, 187, 199), 12, .13),
        'screw': ('Рыба-шуруп', (186, 149, 115), 16, .09),
        'nail': ('Рыба-гвоздь', (196, 199, 218), 8, .19),
        'driver': ('Рыба-отвёртка', (234, 150, 49), 22, .16),
    }
    GLOW = {
        'selftap': (108, 222, 235),
        'screw': (248, 184, 95),
        'nail': (161, 188, 242),
        'driver': (248, 222, 122),
    }
    fish_by_kind = {}
    materials = {}

    def notice(game, message, ticks=95):
        game.notification = message
        game.notification_timer = ticks

    def distance_sq(ax, ay, bx, by):
        return (ax-bx)**2 + (ay-by)**2

    def polished_sprite(kind):
        """Детальная модель без внешних PNG: цветной металлический организм."""
        s = pygame.Surface((176, 88), pygame.SRCALPHA)
        draw = pygame.draw
        black = (25, 35, 49)
        spec = FISH_INFO[kind]
        base = spec[1]
        shine = tuple(min(255, c + 56) for c in base)
        dim = tuple(max(0, c - 64) for c in base)
        fish_blue = (85, 185, 211)
        y = 44
        if kind in ('selftap', 'screw'):
            # Дорсальные/вентральные плавники, выполняющие роль ключа.
            draw.polygon(s, fish_blue, [(61, 37), (80, 12), (92, 35), (118, 28), (110, 44)])
            draw.polygon(s, (44, 119, 159), [(67, 52), (87, 76), (100, 52), (117, 57)])
            draw.polygon(s, black, [(24, 28), (129, 31), (162, 44), (129, 57), (24, 60)])
            draw.polygon(s, base, [(24, 31), (128, 34), (157, 44), (128, 54), (24, 57)])
            draw.line(s, shine, (27, 36), (126, 36), 4)
            draw.line(s, dim, (28, 55), (128, 51), 3)
            # Реалистичная резьба на цилиндре.
            for x in range(48, 129, 11):
                draw.lines(s, black, False, [(x-5, 33), (x+5, 54)], 4)
                draw.lines(s, shine, False, [(x-2, 33), (x+8, 53)], 2)
            # Шляпка и надрез под отвёртку.
            head_color = (125, 141, 157) if kind == 'selftap' else (169, 112, 71)
            draw.rect(s, black, (12, 28, 22, 33), border_radius=5)
            draw.rect(s, head_color, (15, 30, 17, 29), border_radius=4)
            draw.line(s, (239, 226, 200), (22, 33), (22, 57), 2)
            if kind == 'screw':
                draw.line(s, (40, 50, 62), (16, 44), (30, 44), 3)
                draw.line(s, (40, 50, 62), (22, 36), (22, 52), 3)
            else:
                draw.line(s, (40, 50, 62), (16, 44), (30, 44), 3)
            eye_x = 135
        elif kind == 'nail':
            # Уплощённая широкая шляпка + длинное узкое заострённое тело.
            draw.polygon(s, (89, 169, 219), [(80, 38), (94, 17), (111, 37)])
            draw.polygon(s, (80, 126, 177), [(77, 49), (97, 69), (113, 50)])
            draw.polygon(s, black, [(23, 39), (143, 39), (166, 44), (143, 49), (23, 49)])
            draw.polygon(s, base, [(23, 40), (141, 40), (163, 44), (141, 48), (23, 48)])
            draw.line(s, shine, (25, 41), (144, 41), 2)
            draw.rect(s, black, (14, 33, 13, 22), border_radius=3)
            draw.rect(s, (140, 148, 170), (16, 35, 9, 18), border_radius=2)
            for x in (71, 87, 103):
                draw.line(s, dim, (x, 42), (x+3, 47), 1)
            eye_x = 142
        else:
            # Рыба-отвёртка: шестиугольная рукоятка-жаберный корпус.
            draw.polygon(s, fish_blue, [(65, 33), (85, 10), (96, 35), (118, 33)])
            draw.polygon(s, (54, 139, 194), [(67, 56), (84, 78), (106, 52)])
            draw.polygon(s, black, [(14, 40), (18, 27), (81, 27), (96, 38),
                                    (151, 40), (168, 44), (151, 49),
                                    (96, 51), (81, 61), (18, 61)])
            draw.polygon(s, (202, 104, 31), [(16, 39), (19, 31), (81, 31),
                                             (93, 41), (93, 49), (81, 57),
                                             (19, 57)])
            draw.line(s, (255, 200, 100), (24, 34), (79, 34), 5)
            for x in (29, 42, 55, 68):
                draw.line(s, (90, 58, 51), (x, 34), (x+7, 54), 3)
            draw.rect(s, (151, 165, 174), (94, 40, 58, 9))
            draw.line(s, (235, 240, 243), (102, 42), (149, 42), 2)
            draw.polygon(s, (110, 124, 146), [(152, 39), (169, 43), (169, 46), (152, 49)])
            eye_x = 83
        # Глаза остаются узнаваемо живыми, включая биолюминесценцию.
        eye_y = 38 if kind in ('selftap', 'screw') else 39
        draw.circle(s, black, (eye_x, eye_y), 6)
        draw.circle(s, GLOW[kind], (eye_x, eye_y), 4)
        draw.circle(s, (254, 255, 249), (eye_x-1, eye_y-1), 2)
        # Мельчайшие металлические пятна-чешуйки.
        for i in range(12):
            x = 40 + i * 8
            if (kind == 'nail' and i > 10) or (kind == 'driver' and i > 5):
                break
            draw.circle(s, (246, 240, 215, 140), (x, 44), 1)
        return s.convert_alpha()

    def anchor_head_sprite(kind, diameter=176):
        """Вкрученная рыба: круглая шляпка с настоящим крестовым шлицем."""
        s = pygame.Surface((diameter, diameter), pygame.SRCALPHA)
        d = pygame.draw
        cx = cy = diameter // 2
        r = diameter * 0.46
        base = FISH_INFO[kind][1]
        dark = tuple(max(0, c - 90) for c in base)
        pale = tuple(min(255, c + 70) for c in base)
        d.circle(s, (18, 29, 39, 245), (cx + 2, cy + 3), round(r + 2))
        d.circle(s, dark, (cx, cy), round(r))
        d.circle(s, base, (cx, cy), round(r * 0.91))
        for j in range(18):
            radius = r * (0.86 - j * .021)
            shade = tuple(min(255, round(c*(.77+j*.016))) for c in base)
            d.circle(s, shade, (cx, cy), round(radius), max(1,round(diameter / 160)))
        d.circle(s, pale, (cx, cy), round(r*.83), max(1,round(diameter*.024)))
        slot = max(2, round(diameter * .07))
        arm = round(diameter * .26)
        # Тёмная канавка-крест и металлические световые кромки.
        d.line(s, (18, 28, 39), (cx-arm, cy), (cx+arm, cy), slot)
        d.line(s, (18, 28, 39), (cx, cy-arm), (cx, cy+arm), slot)
        off = max(1,round(diameter*.016))
        d.line(s, pale, (cx-arm, cy-slot//2-off), (cx+arm, cy-slot//2-off), off)
        d.line(s, pale, (cx-slot//2-off, cy-arm), (cx-slot//2-off, cy+arm), off)
        d.circle(s, (24, 33, 44), (cx, cy), max(1, slot//3))
        return s.convert_alpha()

    def tool_sprite(kind, amount=1):
        s = pygame.Surface((64, 64), pygame.SRCALPHA)
        d = pygame.draw
        if kind == 'driver':
            d.line(s, (32, 45, 54), (12, 51), (52, 11), 8)
            d.line(s, (187, 202, 212), (22, 41), (52, 11), 5)
            d.line(s, (247, 170, 56), (10, 53), (25, 38), 10)
            d.circle(s, (250, 209, 111), (10, 53), 5)
        else:
            palette = {'selftap': (162, 201, 226), 'screw': (219, 167, 112),
                       'nail': (190, 190, 209), 'broken': (151, 145, 160)}
            color = palette[kind]
            for i in range(min(6, amount)):
                x = 13 + (i % 3) * 17
                y = 12 + (i // 3) * 24
                d.line(s, (43, 49, 64), (x+2, y+2), (x+6, y+19), 6)
                d.line(s, color, (x+2, y+2), (x+6, y+19), 3)
                d.line(s, (245, 229, 207), (x-2, y+2), (x+6, y+2), 3)
        return s.convert_alpha()

    def register():
        for kind, (name, color, hp, speed) in FISH_INFO.items():
            fish = AnimalType(name, color, 32, speed=speed, hp=hp,
                              behavior='neutral', fish_style=True,
                              fish_length=120, fish_width=34,
                              volume=1, edible=False,
                              zigzag_strength=.15 if kind == 'nail' else .08,
                              speed_variation=.23)
            fish.fastener_hd_image = polished_sprite(kind)
            fish.fastener_anchor_hd_image = anchor_head_sprite(kind)
            fish.fastener_anchor_image = pygame.transform.smoothscale(
                fish.fastener_anchor_hd_image, (44, 44)).convert_alpha()
            # Мир имеет клетки ~32 px: рыба не должна быть 5 клеток длиной.
            fish.image = pygame.transform.smoothscale(
                fish.fastener_hd_image, (84, 42)).convert_alpha()
            fish.rotated_cache.clear()
            fish.glow_image = None  # Не подсвечивать металл целиком ночью.
            fish.glow_rotated_cache.clear()
            fish.fastener_kind = kind
            fish_by_kind[kind] = fish
            game_ns['fastener_' + kind + '_type'] = fish

        screwdriver = ObjectType('Отвёртка', (233, 164, 57), 26, 'rect',
                                 is_weapon=True, damage=3,
                                 weapon_range=1.5, weapon_type='ближний бой')
        screwdriver.image = tool_sprite('driver')
        materials['driver'] = screwdriver
        game_ns['fastener_screwdriver_type'] = screwdriver
        for kind, name, count in (('selftap', 'Набор саморезов', 4),
                                  ('screw', 'Набор шурупов', 4),
                                  ('nail', 'Набор гвоздей', 6)):
            item = ObjectType(name, FISH_INFO[kind][1], 23, 'rect')
            item.image = tool_sprite(kind, count)
            materials[kind] = item
            game_ns['fastener_pack_' + kind] = item
        broken = ObjectType('Сломанный саморез', (114, 113, 125), 16, 'rect')
        broken.image = tool_sprite('broken', 2)
        materials['broken'] = broken
        game_ns['fastener_broken_type'] = broken

        # Входные материалы — сами рыбы. Наборы имеют несколько единиц крепежа,
        # но занимают ОДНУ клетку инвентаря.
        recipes = game_ns['recipes']
        recipes.extend([
            Recipe('Отвёртка', {fish_by_kind['driver']: 1,
                                  game_ns['titan_type']: 1}, screwdriver,
                   1, 'Изготовитель', 'Инструменты'),
            Recipe('Набор саморезов (4)', {fish_by_kind['selftap']: 2},
                   materials['selftap'], 1, 'Изготовитель', 'Крепёж'),
            Recipe('Набор шурупов (4)', {fish_by_kind['screw']: 2},
                   materials['screw'], 1, 'Изготовитель', 'Крепёж'),
            Recipe('Набор гвоздей (6)', {fish_by_kind['nail']: 3},
                   materials['nail'], 1, 'Изготовитель', 'Крепёж'),
            Recipe('Титан из сломанного крепежа',
                   {broken: 3}, game_ns['titan_type'], 1,
                   'Изготовитель', 'Переработка'),
        ])
        # Только сложные постройки, а не базовые стены/люки, требуют крепежа.
        fastening = {
            'Многоцелевая комната': {'screw': 1, 'selftap': 1},
            'Большая комната': {'screw': 2, 'selftap': 1},
            'Стыковочная шахта': {'screw': 2, 'selftap': 1},
            'Комната сканирования': {'screw': 1, 'selftap': 1},
            'Обсерватория': {'selftap': 1},
            'Большой аквариум': {'selftap': 1},
            'Биореактор': {'screw': 1},
            'Ядерный реактор': {'screw': 2, 'selftap': 1},
            'Водоочистная станция': {'screw': 1},
            'Теплоэлектростанция': {'screw': 1},
            'Солнечная панель': {'selftap': 1},
            'Стационарный воздушный насос': {'selftap': 1},
            'Консоль улучшения транспорта': {'screw': 1},
            'Модификационная станция': {'screw': 1},
            'Переносной сборщик транспорта': {'screw': 1},
            'Шкаф': {'nail': 1},
            'Стеллаж для растений': {'nail': 1},
            'Внутренняя грядка': {'nail': 1},
            'Наружная грядка': {'nail': 1},
        }
        for recipe in recipes:
            needs = fastening.get(recipe.name)
            if needs is None:
                continue
            for kind, quantity in needs.items():
                ingredient = materials[kind]
                recipe.ingredients[ingredient] = (
                    recipe.ingredients.get(ingredient, 0) + quantity)
            recipe.requires_fastener_tool = True
            recipe.result.requires_fastener_assembly = True

        # Каждая особь попадает в реальные игровые биомы,
        # а не только в интерфейс карты.
        for biome in game_ns['biomes']:
            if game_ns['is_land_biome'](biome) or getattr(biome, 'aurora_style', False):
                continue
            densities = BIOMES.get(biome.name)
            if densities:
                for kind, count in zip(KINDS, densities):
                    if count:
                        biome.animals.append((fish_by_kind[kind], count))

        registry = game_ns.get('START_ITEM_REGISTRY')
        if isinstance(registry, dict):
            for key, val in list(fish_by_kind.items()) + list(materials.items()):
                registry[val.name] = val
                registry['fastener_' + key] = val

    old_init_assets = game_ns['init_assets']

    def enhanced_init_assets():
        old_init_assets()
        fish_by_kind.clear()
        materials.clear()
        register()

    game_ns['init_assets'] = enhanced_init_assets

    old_recipe_can_craft = Recipe.can_craft

    def fastener_can_craft(self, counts):
        if getattr(self, 'requires_fastener_tool', False):
            tool = materials.get('driver')
            if tool is None or counts.get(tool, 0) < 1:
                return False
        return old_recipe_can_craft(self, counts)

    Recipe.can_craft = fastener_can_craft

    # Защитить прикрученную рыбу от любого источника урона, даже торпед,
    # урона питомцев и прочих модулей, пока её не выкрутили.
    def protected_get_hp(self):
        return self.__dict__.get('_fastener_protected_hp', self.__dict__.get('hp', 0))

    def protected_set_hp(self, value):
        previous = self.__dict__.get('_fastener_protected_hp', self.__dict__.get('hp', value))
        if (getattr(self, 'fastener_anchored', False)
                and getattr(getattr(self, 'type', None), 'fastener_kind', None) in KINDS
                and value < previous):
            return
        self.__dict__['_fastener_protected_hp'] = value

    Animal.hp = property(protected_get_hp, protected_set_hp)

    old_rotation = Animal._update_rotation

    def fastener_rotation(self):
        if (getattr(self, 'fastener_anchored', False)
                and getattr(self.type, 'fastener_anchor_image', None) is not None):
            self.rotated_image = self.type.fastener_anchor_image
            self.rotated_glow = None
            self.visual_angle = 0
            return
        return old_rotation(self)

    Animal._update_rotation = fastener_rotation

    old_animal_update = Animal.update

    def fastener_animal_update(self, player_x, player_y, world):
        if getattr(self, 'fastener_anchored', False) and not getattr(
                self, 'is_companion', False):
            self.active = (abs(self.x-player_x) < game_ns['VIEW_RADIUS'] + 3
                           and abs(self.y-player_y) < game_ns['VIEW_RADIUS'] + 3)
            self.dx = self.dy = 0
            return
        return old_animal_update(self, player_x, player_y, world)

    Animal.update = fastener_animal_update

    def _is_fastener(animal):
        return getattr(animal.type, 'fastener_kind', None) in KINDS

    def _unfasten(animal, frame):
        animal.fastener_anchored = False
        animal.fastener_anchor = None
        animal.fastener_next_anchor = frame + random.randint(180, 450)
        animal.dx = math.cos(animal.swim_heading) * animal.type.speed
        animal.dy = math.sin(animal.swim_heading) * animal.type.speed
        animal._update_rotation()

    def _anchor_target(game, fish):
        # Один «поиск отверстия» выполняется не чаще одного раза за 30 кадров.
        # Вводится различие характеров: гвоздь любит обломки/постройки,
        # саморез — кораллы и водоросли, шуруп — конструкции.
        cx, cy = int(fish.x), int(fish.y)
        flora = game._current_flora_grid()
        structures = game._get_current_object_grid()
        kind = fish.type.fastener_kind
        candidates = []
        for y in range(cy-1, cy+2):
            for x in range(cx-1, cx+2):
                dist = distance_sq(fish.x, fish.y, x+.5, y+.5)
                if dist > 1.1:
                    continue
                plant = flora.get((x, y))
                if plant is not None and kind in ('selftap', 'screw'):
                    candidates.append((dist + (.25 if kind == 'screw' else 0), x, y,
                                       'flora'))
                obj = structures.get((x, y))
                if obj is not None and (kind in ('nail', 'screw')
                                        or getattr(obj, 'is_buildable', False)):
                    candidates.append((dist + (-.1 if kind == 'nail' else 0),
                                       x, y, 'object'))
        if not candidates:
            return None
        _, x, y, category = min(candidates)
        return x, y, category

    def _fastener_step(game):
        frame = getattr(game, 'frame_count', 0)
        if game.state != 'playing' or getattr(game, 'settings_open', False):
            return
        stuck = getattr(game, '_fastener_player_stuck', 0)
        if stuck and frame % 140 == 0:
            game._damage_player_or_vehicle(min(3, 1 + stuck // 2))
            notice(game, f'В вас вкручено рыб-саморезов: {stuck}! '
                         'Выберите отвёртку и нажмите U', 80)
        if frame % 12 != 0:
            return
        nearby = [a for a in game.get_nearby_animals(10)
                  if a.hp > 0 and _is_fastener(a)
                  and not getattr(a, 'is_companion', False)]
        if not nearby:
            return
        active_world = game.cave_world if game.in_cave else game.world
        drivers = [a for a in nearby if a.type.fastener_kind == 'driver']
        targets = [a for a in nearby if a.type.fastener_kind != 'driver']
        # Хищная отвертка определяет ближайшую цель (даже закрученную).
        for driver in drivers:
            victim = min(targets, key=lambda a: distance_sq(a.x, a.y, driver.x, driver.y),
                         default=None)
            if victim is None:
                continue
            ds = distance_sq(victim.x, victim.y, driver.x, driver.y)
            if ds > 6.5 ** 2:
                continue
            if ds < .8 ** 2:
                if getattr(victim, 'fastener_anchored', False):
                    # Кооператив: три и более рыбы, зацепившиеся за одно
                    # сооружение, мешают природной отвёртке их выкрутить.
                    anchor = getattr(victim, 'fastener_anchor', None)
                    allies = sum(1 for other in targets
                                 if getattr(other, 'fastener_anchored', False)
                                 and getattr(other, 'fastener_anchor', None) == anchor)
                    if anchor is not None and anchor[2] == 'object' \
                            and allies >= 3 and rand.random() < .72:
                        driver.fastener_hunt_cooldown = frame + 80
                        continue
                    _unfasten(victim, frame)
                    if distance_sq(victim.x, victim.y, game.player_x,
                                   game.player_y) < 5:
                        notice(game, 'Рыба-отвёртка выкрутила '
                                     + victim.type.name.lower() + '!', 80)
                # Отвёртка разгоняет соседей, но не уничтожает весь вид.
                victim.fastener_next_anchor = frame + 180
                victim.swim_heading = math.atan2(victim.y-driver.y,
                                                   victim.x-driver.x)
                victim.cruise_heading = victim.swim_heading
                driver.fastener_hunt_cooldown = frame + 100
                continue
            if frame < getattr(driver, 'fastener_hunt_cooldown', 0):
                continue
            old_x, old_y = driver.x, driver.y
            heading = math.atan2(victim.y-driver.y, victim.x-driver.x)
            step = driver.type.speed * 1.45 * 12
            nx = driver.x + math.cos(heading)*min(step, math.sqrt(ds))
            ny = driver.y + math.sin(heading)*min(step, math.sqrt(ds))
            # Делим ускорение на маленькие микрошаги: сквозь стену не плавать.
            segments = max(1, math.ceil(math.hypot(nx-driver.x,ny-driver.y)/.15))
            for i in range(segments):
                px = old_x+(nx-old_x)*(i+1)/segments
                py = old_y+(ny-old_y)*(i+1)/segments
                if driver._can_swim_to(px, py, active_world):
                    driver.x, driver.y = px, py
                else:
                    break
            if not game.in_cave:
                game.world.update_animal_index(driver, old_x, old_y)
            driver.dx = math.cos(heading)*driver.type.speed
            driver.dy = math.sin(heading)*driver.type.speed
            driver.swim_heading = heading
            driver._update_rotation()
        # Рыбы-гвозди сбиваются в косяк и синхронизируют направление рывков.
        nails = [a for a in targets if a.type.fastener_kind == 'nail'
                 and not getattr(a, 'fastener_anchored', False)]
        for nail in nails:
            mates = [b for b in nails if b is not nail and
                     distance_sq(b.x, b.y, nail.x, nail.y) < 2.2**2]
            if len(mates) >= 2:
                ax = math.cos(nail.swim_heading) + sum(
                    math.cos(b.swim_heading) for b in mates)
                ay = math.sin(nail.swim_heading) + sum(
                    math.sin(b.swim_heading) for b in mates)
                desired = math.atan2(ay, ax)
                nail.cruise_heading = (nail.cruise_heading * .55
                                      + desired * .45)
        # Экологическая механика самостоятельного закручивания.
        for fish in targets:
            if getattr(fish, 'fastener_anchored', False):
                anchor = getattr(fish, 'fastener_anchor', None)
                if anchor is not None:
                    ax, ay, category = anchor
                    substrate = (game._current_flora_grid() if category == 'flora'
                                 else game._get_current_object_grid())
                    if substrate.get((ax, ay)) is None:
                        _unfasten(fish, frame)
                continue
            if frame < getattr(fish, 'fastener_next_anchor', 0):
                continue
            if rand.random() > .17:
                continue
            # Саморез способен вкрутиться прямо в игрока.
            if (fish.type.fastener_kind == 'selftap'
                    and distance_sq(fish.x, fish.y, game.player_x,
                                    game.player_y) < .75 ** 2
                    and rand.random() < .09):
                if rand.random() < .075:
                    game.remove_current_animal(fish)
                    game._current_dropped_items().setdefault(
                        (int(fish.x), int(fish.y)), []).append(materials['broken'])
                    notice(game, 'Саморез сломался при завинчивании!', 95)
                else:
                    game.remove_current_animal(fish)
                    game._fastener_player_stuck = min(6,
                          getattr(game, '_fastener_player_stuck', 0) + 1)
                    notice(game, 'Рыба-саморез вкрутилась в вас! '
                                 'Отвёртка + U для извлечения.', 110)
                continue
            target = _anchor_target(game, fish)
            if target is None:
                continue
            x, y, surface = target
            if (fish.type.fastener_kind == 'selftap'
                    and rand.random() < .03):
                game.remove_current_animal(fish)
                game._current_dropped_items().setdefault(
                    (int(fish.x), int(fish.y)), []).append(materials['broken'])
                continue
            old_x, old_y = fish.x, fish.y
            fish.x, fish.y = x+.5, y+.5
            fish.fastener_anchored = True
            fish.fastener_anchor = (x, y, surface)
            fish.fastener_next_anchor = frame + 900
            # При изменении чанка важно обновить пространственный индекс.
            if not game.in_cave:
                game.world.update_animal_index(fish, old_x, old_y)
            fish.dx = fish.dy = 0
            fish._update_rotation()
        # Скреплённые «рыболюбы» придают постройке дополнительную прочность.
        # Прочность носит декоративный/защитный характер (в оригинале у
        # построек нет числовых HP), зато охотники могут освободить рыб.

    old_game_init = Game.__init__

    def enhanced_game_init(self, *args, **kwargs):
        old_game_init(self, *args, **kwargs)
        self._fastener_player_stuck = 0
        # В пещерах обитают собственные популяции, независимо от поверхности.
        count = 0
        for tunnel in getattr(self, 'cave_tunnels', []):
            if count >= 55:
                break
            if not hasattr(tunnel, 'random_point'):
                continue
            if random.random() > .23:
                continue
            for _ in range(10):
                x, y = tunnel.random_point()
                if self.get_cave_tunnel_at(x, y) is not tunnel:
                    continue
                if self.cave_world.get_object_at(x, y) is not None:
                    continue
                kind = random.choices(KINDS, weights=(4, 3, 3, 2), k=1)[0]
                a = Animal(x, y, fish_by_kind[kind], tunnel)
                a.fastener_next_anchor = self.frame_count + random.randint(90, 480)
                self.cave_animals.append(a)
                count += 1
                break
        # Игрок может прописать новых рыб или отвёртку в START_INVENTORY.

    Game.__init__ = enhanced_game_init

    old_game_update = Game.update

    def enhanced_game_update(self, *args, **kwargs):
        old_game_update(self, *args, **kwargs)
        if getattr(self, 'running', True):
            _fastener_step(self)

    Game.update = enhanced_game_update

    old_game_use = Game.use_item

    def enhanced_game_use(self, *args, **kwargs):
        ix = getattr(self, 'active_slot_index', -1)
        if ix is None or ix < 0 or ix >= len(self.inventory):
            return old_game_use(self, *args, **kwargs)
        if self.inventory[ix] is not materials.get('driver'):
            return old_game_use(self, *args, **kwargs)
        if self.state != 'playing':
            return
        if getattr(self, '_fastener_player_stuck', 0) > 0:
            self._fastener_player_stuck -= 1
            notice(self, 'Саморез выкручен! Осталось: '
                         + str(self._fastener_player_stuck))
            return
        close = [a for a in self.get_nearby_animals(2.2)
                 if a.hp > 0 and _is_fastener(a)
                 and getattr(a, 'fastener_anchored', False)
                 and not getattr(a, 'is_companion', False)
                 and distance_sq(a.x, a.y, self.player_x,
                                 self.player_y) <= 2.2**2]
        if close:
            fish = min(close, key=lambda a: distance_sq(a.x,a.y,
                                                        self.player_x,self.player_y))
            if self.add_to_inventory(fish.type, wild_catch=True):
                self.remove_current_animal(fish)
                notice(self, f'Отвёрткой выкручен и пойман: {fish.type.name}')
            else:
                notice(self, 'Инвентарь полон: рыбку выкрутить некуда')
            return
        notice(self, 'Нет вкрученной рыбы поблизости (радиус 2,2 клетки)')

    Game.use_item = enhanced_game_use

    # При закреплении иконка сразу становится круглой. При выкручивании
    # обычная плавательная модель вновь появляется в исходных пропорциях.
    old_game_pick_up = Game.pick_up

    def enhanced_pick_up(self, *args, **kwargs):
        # Прикрученных рыб нельзя подобрать без инструмента.
        for fish in self.get_nearby_animals(1.15):
            if (_is_fastener(fish) and fish.hp > 0
                    and getattr(fish, 'fastener_anchored', False)
                    and int(fish.x) == int(self.player_x)
                    and int(fish.y) == int(self.player_y)):
                notice(self, 'Рыба прикручена! Возьмите отвёртку и нажмите U')
                return
        return old_game_pick_up(self, *args, **kwargs)

    Game.pick_up = enhanced_pick_up

    old_game_build = Game.build

    def enhanced_build(self, *args, **kwargs):
        ix = getattr(self, 'active_slot_index', -1)
        if ix is not None and 0 <= ix < len(self.inventory):
            item = self.inventory[ix]
            if (item is not None
                    and getattr(item, 'requires_fastener_assembly', False)
                    and self.inventory_counts.get(materials.get('driver'), 0) == 0):
                notice(self, 'Для сборки сложной конструкции нужна отвёртка!')
                return
        return old_game_build(self, *args, **kwargs)

    Game.build = enhanced_build

    old_game_attack = Game.attack

    def enhanced_attack(self, *args, **kwargs):
        if self.equipped_weapon is materials.get('driver'):
            previous = self.player_attack_damage
            self.player_attack_damage = 3  # слабее обычного ножа
            try:
                return old_game_attack(self, *args, **kwargs)
            finally:
                self.player_attack_damage = previous
        return old_game_attack(self, *args, **kwargs)

    Game.attack = enhanced_attack

    # Информация при сканировании видов хранится в читаемом виде;
    # действующие игровые справочники обновляются без удаления старых данных.
    info = {
        'Рыба-саморез': 'Спиральный организм. Завинчивается в водоросли, кораллы, постройки и иногда в дайверов; при заклинивании ломается.',
        'Рыба-шуруп': 'Более медленный родственник самореза. Любит древесину и затонувшие конструкции. Служит сырьём для набора шурупов.',
        'Рыба-гвоздь': 'Самая быстрая из крепёжных рыб. Специализируется на застревании в предметах и собирается в опасные косяки.',
        'Рыба-отвёртка': 'Природный враг крепёжных рыб. Выкручивает тех из укрытий. Пойманная особь служит основой многоразового инструмента.',
    }
    game_ns['FASTENER_FISH_DESCRIPTIONS'] = info
    game_ns['FASTENER_FISH_BIOMES'] = BIOMES
    game_ns['FASTENER_FISH_TYPES'] = fish_by_kind
    game_ns['FASTENER_FISH_MATERIALS'] = materials


# В самом конце установщик добавляет: install_fastener_fish(globals())

install_fastener_fish(globals())
# <<< FASTENER_FISH_MOD_V1 <<<


# >>> FUNGUS_NUCLI_MOD_V1: Ядрёный гриб >>>
FUNGUS_NUCLI_NAME = 'Ядрёный гриб'
FUNGUS_NUCLI_LATIN = 'Fungus nucli'
FUNGUS_NUCLI_HP = 10
FUNGUS_NUCLI_DAMAGE = 250
FUNGUS_NUCLI_METERS_PER_CELL = 10.0
FUNGUS_NUCLI_RADIUS_METERS = 50.0
FUNGUS_NUCLI_RADIUS_CELLS = FUNGUS_NUCLI_RADIUS_METERS / FUNGUS_NUCLI_METERS_PER_CELL
FUNGUS_NUCLI_FUSE_SECONDS = (10.0, 15.0)
FUNGUS_NUCLI_SELF_DESTRUCT_DISTANCE = 1.4


def generate_fungus_nucli_sprite(size=48):
    """Детализированный гриб сверху: радиальные пластинки и ядерное ядро."""
    size = max(34, int(size))
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    cx = cy = size // 2
    r = size * 0.38
    draw = pygame.draw
    draw.circle(surf, (23, 43, 25), (cx, cy), round(r + 2))
    for i in range(16):
        a = math.tau * i / 16
        a2 = a + 0.16
        outer = (round(cx + r * math.cos(a)), round(cy + r * math.sin(a)))
        inner = (round(cx + r * 0.27 * math.cos(a2)), round(cy + r * 0.27 * math.sin(a2)))
        draw.line(surf, (72, 116, 55) if i % 2 else (134, 160, 44), inner, outer, max(2, size // 20))
    draw.circle(surf, (39, 74, 38), (cx, cy), round(r * 0.7))
    draw.circle(surf, (128, 174, 53), (cx, cy), round(r * 0.54), 2)
    for i in range(7):
        a = math.tau * i / 7
        x = round(cx + math.cos(a) * r * 0.48)
        y = round(cy + math.sin(a) * r * 0.48)
        draw.circle(surf, (211, 220, 65), (x, y), max(2, size // 17))
        draw.circle(surf, (247, 250, 150), (x - 1, y - 1), 1)
    draw.circle(surf, (9, 35, 19), (cx, cy), round(r * 0.29))
    draw.circle(surf, (112, 255, 65), (cx, cy), round(r * 0.21))
    draw.circle(surf, (225, 255, 152), (cx - 2, cy - 2), max(2, size // 13))
    return surf


def install_fungus_nucli(game_ns):
    original_assets = game_ns['init_assets']

    def init_assets_with_fungus():
        original_assets()
        fungus = AnimalType(FUNGUS_NUCLI_NAME, (120, 205, 54), 42,
                            speed=0.0, behavior='neutral', hp=FUNGUS_NUCLI_HP,
                            damage=0, edible=False, volume=1)
        fungus.image = generate_fungus_nucli_sprite(46)
        fungus.glow_image = generate_bioluminescence_sprite(
            fungus.image, 'acid_mushroom', is_plant=True)
        game_ns['fungus_nucli_type'] = fungus
        game_ns['FUNGUS_NUCLI_DESCRIPTIONS'] = {
            FUNGUS_NUCLI_NAME: ('Fungus nucli. Неподвижное взрывное существо, 10 HP. '
                                'После гибели либо самоуничтожения детонирует через 10–15 секунд. '
                                'Радиус 50 м, урон 250 HP.')}
    game_ns['init_assets'] = init_assets_with_fungus

    old_generate_animals = World._generate_animals

    def generate_animals_with_fungus(self):
        old_generate_animals(self)
        fungus = game_ns['fungus_nucli_type']
        rng = random.Random(734175)
        for biome in self.biomes:
            if not biome.passable or is_land_biome(biome):
                continue
            if not any(token in biome.name.lower() for token in
                       ('риф', 'биолюмин', 'гриб', 'глубин', 'вулкан')):
                continue
            quantity = min(16, max(2, int(biome.area() / 800)))
            for _ in range(quantity):
                for _attempt in range(60):
                    x, y = biome.random_point(rng)
                    if self.get_biome_at(int(x), int(y)) is biome:
                        self.animals.append(Animal(x, y, fungus, biome))
                        break
    World._generate_animals = generate_animals_with_fungus

    original_death = Game.handle_animal_death

    def nuclear_death(self, animal):
        if animal.type.name != FUNGUS_NUCLI_NAME:
            return original_death(self, animal)
        if getattr(animal, 'fungus_armed', False):
            return
        animal.fungus_armed = True
        animal.hp = 0
        animal.fungus_detonation_at = pygame.time.get_ticks() + random.randint(10000, 15000)
        animal.fungus_in_cave = self.in_cave
        self.nuclear_fuses.append(animal)
        self.notification = 'Ядрёный гриб взведён! Покиньте радиус 50 м!'
        self.notification_timer = 135

    Game.handle_animal_death = nuclear_death
    old_game_init = Game.__init__

    def init_with_fungus(self):
        old_game_init(self)
        self.nuclear_fuses = []
        self.nuclear_blasts = []
        self.nuclear_craters = []  # (world_x, world_y, cave, expiry_ms)
        self.nuclear_flashes = []  # время вспышки, координаты и уровень
    Game.__init__ = init_with_fungus
    old_game_update = Game.update

    def update_with_fungus(self):
        old_game_update(self)
        if self.state != 'playing' or self.settings_open:
            return
        now = pygame.time.get_ticks()
        if not self.in_cave:
            for animal in self.world.get_animals_near(self.player_x, self.player_y, 2.5):
                if (animal.type.name == FUNGUS_NUCLI_NAME and animal.hp > 0
                        and not getattr(animal, 'fungus_armed', False)
                        and math.hypot(animal.x - self.player_x,
                                       animal.y - self.player_y) <= FUNGUS_NUCLI_SELF_DESTRUCT_DISTANCE):
                    self.handle_animal_death(animal)
        for fungus in self.nuclear_fuses[:]:
            if now < fungus.fungus_detonation_at:
                continue
            self.nuclear_fuses.remove(fungus)
            self.nuclear_blasts.append((fungus.x, fungus.y, now + 850, fungus.fungus_in_cave))
            self.nuclear_craters.append((fungus.x, fungus.y, fungus.fungus_in_cave, now + 10000))
            self.nuclear_flashes.append((now, fungus.x, fungus.y, fungus.fungus_in_cave))
            # Урон по игроку и его транспорту — только на той же карте/уровне.
            if self.in_cave == fungus.fungus_in_cave and math.hypot(
                    self.player_x - fungus.x, self.player_y - fungus.y) <= FUNGUS_NUCLI_RADIUS_CELLS:
                self._damage_player_or_vehicle(FUNGUS_NUCLI_DAMAGE)
            population = self.cave_animals if fungus.fungus_in_cave else self.world.animals
            for other in tuple(population):
                if other is fungus or other.hp <= 0:
                    continue
                if math.hypot(other.x - fungus.x, other.y - fungus.y) <= FUNGUS_NUCLI_RADIUS_CELLS:
                    other.hp -= FUNGUS_NUCLI_DAMAGE
                    if other.hp <= 0:
                        # Взрыв грибов вызывает цепное взведение с новой задержкой.
                        if other.type.name == FUNGUS_NUCLI_NAME:
                            self.handle_animal_death(other)
                        else:
                            # Трупы не выдаются игроку автоматически.
                            self.remove_current_animal(other) if (self.in_cave == fungus.fungus_in_cave) else (
                                population.remove(other) if other in population else None)
            if fungus in population:
                population.remove(fungus)
            if not fungus.fungus_in_cave:
                self.world.rebuild_animal_index()
            self.notification = 'Ядрёный гриб: ВЗРЫВ!'
            self.notification_timer = 75
        self.nuclear_craters = [c for c in self.nuclear_craters if c[3] > now]
        self.nuclear_blasts = [b for b in self.nuclear_blasts if b[2] > now]
        self.nuclear_flashes = [f for f in self.nuclear_flashes if now - f[0] < 4000]
    Game.update = update_with_fungus

    original_render = Game.render

    def render_with_fungus(self):
        original_render(self)
        if self.state != 'playing':
            return
        now = pygame.time.get_ticks()
        camera_x = self.player_x - getattr(self, '_render_view_radius', VIEW_RADIUS)
        camera_y = self.player_y - getattr(self, '_render_view_radius', VIEW_RADIUS)
        for fungus in self.nuclear_fuses:
            if fungus.fungus_in_cave != self.in_cave:
                continue
            sx = int((fungus.x - camera_x) * CELL_SIZE + CELL_SIZE / 2)
            sy = int((fungus.y - camera_y) * CELL_SIZE + CELL_SIZE / 2)
            if not (-90 <= sx <= SCREEN_SIZE + 90 and -90 <= sy <= SCREEN_SIZE + 90):
                continue
            seconds = max(0, (fungus.fungus_detonation_at - now) / 1000)
            flashing = int(now / max(75, 430 * seconds / 15)) % 2 == 0
            col = (255, 55, 32) if flashing else (255, 215, 65)
            pygame.draw.circle(self.screen, col, (sx, sy), 13, 3)
            label = self.small_font.render(f'{seconds:.1f} с', True, col)
            self.screen.blit(label, label.get_rect(center=(sx, sy - 28)))
        for x, y, expiry, cave in self.nuclear_blasts:
            if cave != self.in_cave:
                continue
            progress = 1 - max(0, expiry - now) / 850
            sx = int((x - camera_x) * CELL_SIZE + CELL_SIZE / 2)
            sy = int((y - camera_y) * CELL_SIZE + CELL_SIZE / 2)
            radius = max(1, int(FUNGUS_NUCLI_RADIUS_CELLS * CELL_SIZE * progress))
            pygame.draw.circle(self.screen, (255, 205, 65), (sx, sy), radius, 4)
            pygame.draw.circle(self.screen, (255, 105, 25), (sx, sy), max(2, radius // 2), 3)
        # Эти эффекты рисуются на итоговой поверхности, чтобы зум и интерфейс
        # не мешали общей вспышке. Воронка исчезает через 10 секунд.
        display = pygame.display.get_surface()
        zoom = getattr(self, 'camera_zoom', 1.0)
        for x, y, cave, expiry in self.nuclear_craters:
            if cave != self.in_cave:
                continue
            sx = int(display.get_width() / 2 + (x - self.player_x) * CELL_SIZE * zoom)
            sy = int(display.get_height() / 2 + (y - self.player_y) * CELL_SIZE * zoom)
            radius = max(4, int(1.35 * CELL_SIZE * zoom))
            if sx < -radius or sy < -radius or sx > display.get_width()+radius or sy > display.get_height()+radius:
                continue
            pygame.draw.circle(display, (32, 24, 19), (sx, sy), radius)
            pygame.draw.circle(display, (94, 65, 43), (sx, sy), radius, max(2, int(5*zoom)))
            pygame.draw.circle(display, (12, 16, 19), (sx, sy), max(2, int(radius*.67)))
            for a in range(12):
                angle = math.tau*a/12
                x1 = sx + math.cos(angle)*radius*.84
                y1 = sy + math.sin(angle)*radius*.84
                x2 = sx + math.cos(angle)*radius*1.22
                y2 = sy + math.sin(angle)*radius*1.22
                pygame.draw.line(display, (89, 62, 45), (x1,y1), (x2,y2), max(1,int(2*zoom)))
        # 4 секунды: начальная ослепительная вспышка и нелинейное затухание.
        # Взрывы на другом уровне не ослепляют игрока.
        strengths = [(1.0 - (now - t)/4000.0)**2 for t,x,y,cave in self.nuclear_flashes
                     if cave == self.in_cave and now-t < 4000
                     and math.hypot(x-self.player_x, y-self.player_y) <= 16]
        if strengths:
            alpha = min(245, int(245 * max(strengths)))
            flash = pygame.Surface(display.get_size(), pygame.SRCALPHA)
            flash.fill((255, 247, 213, alpha))
            display.blit(flash, (0, 0))
        if self.nuclear_fuses or self.nuclear_blasts or self.nuclear_flashes or self.nuclear_craters:
            pygame.display.flip()
    Game.render = render_with_fungus

install_fungus_nucli(globals())
# <<< FUNGUS_NUCLI_MOD_V1 <<<


# >>> AVIS_PACHOMICA_MOD_V1: Птица Говносрун и горох >>>
# 1 г гороха -> 1 т токсичного помёта; 100 HP за тонну.
DUNGBIRD_NAME = 'Птица Говносрун'
DUNGBIRD_LATIN = 'Avis pachomica'
DUNGBIRD_SPEED = 4.0 / 60.0       # 4 игровые клетки в секунду при 60 FPS
DUNGBIRD_MAX_TONNES = 10.0
DUNGBIRD_DAMAGE_PER_TONNE = 100
DUNGBIRD_BURST_MIN = 10
DUNGBIRD_BURST_MAX = 30
DUNGBIRD_SHOT_INTERVAL_MS = 500     # Полсекунды между соседними шарами.
DUNGBIRD_SHOT_SPEED = 0.32
DUNGBIRD_SPREAD_RADIANS = math.radians(14)
DUNGBIRD_FRAGMENT_MIN = 3
DUNGBIRD_FRAGMENT_MAX = 7
DUNGBIRD_MAX_ACTIVE_PROJECTILES = 850
PEA_REGROW_MS = 25000


def generate_pea_plant_sprite(size=38, ripe=True):
    """Небольшой куст гороха с усиками, листьями и созревающими стручками."""
    s = max(28, int(size))
    surf = pygame.Surface((s, s), pygame.SRCALPHA)
    d = pygame.draw
    k = s / 40.0

    def pt(x, y):
        return (round(x * k), round(y * k))

    d.line(surf, (47, 112, 47), pt(20, 37), pt(19, 8), max(2, round(3*k)))
    for x, y, endx, endy in ((19, 28, 8, 19), (19, 23, 32, 14),
                            (19, 16, 10, 7), (20, 13, 30, 7)):
        d.line(surf, (60, 139, 57), pt(x, y), pt(endx, endy), max(1, round(2*k)))
        cx, cy = pt(endx, endy)
        rx = max(3, round(7*k))
        ry = max(2, round(4*k))
        d.ellipse(surf, (53, 160, 68), (cx-rx, cy-ry, 2*rx, 2*ry))
        d.ellipse(surf, (108, 199, 80), (cx-rx+1, cy-ry, rx+2, ry))
    for coords in (((19, 9), (21, 3), (25, 3)), ((32, 14), (36, 17), (34, 20))):
        d.lines(surf, (127, 196, 91), False, [pt(*p) for p in coords], max(1, round(k)))
    if ripe:
        for x, y, tilt in ((7, 21, -1), (28, 23, 1), (15, 8, -1)):
            stem_end = pt(x + 3 * tilt, y - 5)
            d.line(surf, (43, 114, 47), stem_end, pt(x, y), max(1, round(k)))
            px, py = pt(x, y)
            w, h = max(5, round(9*k)), max(8, round(13*k))
            d.ellipse(surf, (39, 116, 39), (px-w//2, py-h//2, w, h))
            d.ellipse(surf, (120, 215, 68), (px-w//2+1, py-h//2, max(3,w-2), h-2))
            for off in (-3, 0, 3):
                gx, gy = pt(x, y+off)
                d.circle(surf, (168, 233, 90), (gx, gy), max(1, round(1.4*k)))
    return surf


def generate_dungbird_sprite(size=34):
    """Вид сверху: коричневая птица с крыльями, хвостом и жёлтым клювом."""
    s = max(30, int(size))
    surf = pygame.Surface((s, s), pygame.SRCALPHA)
    d = pygame.draw
    k = s / 40.0

    def pt(x, y):
        return (round(x*k), round(y*k))

    def polygon(color, coords):
        d.polygon(surf, color, [pt(x, y) for x, y in coords])

    polygon((84, 72, 60), [(12, 19), (3, 12), (1, 8), (11, 14), (13, 11), (17, 21)])
    polygon((94, 81, 68), [(12, 22), (2, 30), (2, 35), (12, 29), (16, 26)])
    polygon((106, 93, 81), [(14, 19), (7, 3), (12, 6), (22, 17), (23, 22)])
    polygon((111, 95, 80), [(14, 23), (9, 38), (14, 35), (23, 24)])
    polygon((172, 155, 132), [(15, 13), (24, 13), (31, 17), (31, 23),
                                (23, 29), (15, 26), (12, 20)])
    polygon((222, 202, 166), [(26, 16), (32, 17), (33, 22), (25, 22)])
    polygon((229, 181, 64), [(31, 18), (39, 20), (31, 23)])
    d.circle(surf, (22, 23, 18), pt(29, 16), max(1, round(1.6*k)))
    d.circle(surf, (249, 242, 212), pt(29, 16), 1)
    return surf


def install_dungbird_and_peas(game_ns):
    if game_ns.get('_DUNGBIRD_PEA_INSTALLED'):
        return
    game_ns['_DUNGBIRD_PEA_INSTALLED'] = True

    old_init_assets = game_ns['init_assets']

    def init_assets_with_peas():
        old_init_assets()
        # Используем штатные типы: горох попадает в сканер и меню посадки.
        pea = FloraType('Горох', 'acid_mushroom', 38, 31415)
        pea.kind = 'pea'
        pea.image = generate_pea_plant_sprite(38, True)
        pea.glow_image = None
        bare = FloraType('Горох', 'acid_mushroom', 38, 31416)
        bare.kind = 'pea'
        bare.image = generate_pea_plant_sprite(38, False)
        bare.glow_image = None
        flora_types['pea'] = pea
        pod = ObjectType('Стручок гороха', (98, 198, 66), 16,
                         edible=True, hunger_value=6, thirst_value=2)
        pod.image = pygame.transform.smoothscale(generate_pea_plant_sprite(38, True), (24, 24))
        bird = AnimalType(DUNGBIRD_NAME, (163, 139, 113), 25,
                          speed=DUNGBIRD_SPEED, behavior='neutral', hp=25,
                          damage=0, edible=False, volume=1)
        bird.is_dungbird = True
        bird.fish_style = True  # спрайт ориентирован клювом вправо
        bird.image = generate_dungbird_sprite(34)
        bird.glow_image = None
        bird.inflated_cache = {}
        game_ns['pea_flora_type'] = pea
        game_ns['pea_bare_type'] = bare
        game_ns['pea_pod_type'] = pod
        game_ns['dungbird_type'] = bird

    game_ns['init_assets'] = init_assets_with_peas

    old_generate_flora = World._generate_flora

    def generate_flora_with_peas(self):
        old_generate_flora(self)
        self.pea_patches = {}
        self.pea_ready_at = {}
        rng = random.Random(20261025)
        densities = {'Леса': 8.0, 'Острова': 5.0, 'Побережье': 4.0, 'Горы': 2.0}
        pea = game_ns['pea_flora_type']
        bird_type = game_ns['dungbird_type']
        for biome in self.biomes:
            if not biome.passable or biome.name not in densities:
                continue
            count = min(230, max(3, round(biome.area()/400.0*densities[biome.name])))
            positions = []
            for _ in range(count * 14):
                if len(positions) >= count:
                    break
                rx, ry = biome.random_point(rng)
                key = (int(rx), int(ry))
                if (self.get_biome_at(*key) is not biome or key in self.object_grid
                        or key in self.flora_grid or key in self.tree_grid):
                    continue
                self.flora_grid[key] = pea
                positions.append(key)
            if not positions:
                continue
            self.pea_patches[biome] = positions
            # Стаи рядом с кормовыми растениями; обычная подводная фауна не меняется.
            for _ in range(min(11, max(2, len(positions)//24))):
                px, py = rng.choice(positions)
                bird = Animal(px+0.5, py+0.5, bird_type, biome)
                bird.pea_target = (px, py)
                bird.poop_stock_tonnes = 0.0
                bird.poop_charge_started_at = 0
                bird.poop_next_attack_at = 0
                bird.poop_burst_remaining = 0
                bird.poop_burst_next_at = 0
                bird.poop_burst_mass_each = 0.0
                bird.poop_burst_total = 0
                bird.pea_next_feed_at = 0
                self.animals.append(bird)

    World._generate_flora = generate_flora_with_peas

    original_rotation = Animal._update_rotation

    def bird_rotation(self):
        if not getattr(self.type, 'is_dungbird', False):
            return original_rotation(self)
        angle = -math.degrees(math.atan2(self.dy, self.dx)) if self.dx or self.dy else 0
        # Размер меняется только в момент зарядки (0.25 м -> несколько метров).
        started = getattr(self, 'poop_charge_started_at', 0)
        bursting = getattr(self, 'poop_burst_remaining', 0) > 0
        elapsed = (1800 if bursting else
                   max(0, pygame.time.get_ticks()-started) if started else 0)
        stage = min(8, int(8 * elapsed / 1800)) if (started or bursting) else 0
        rotation = round(angle / 15) * 15
        cache_key = (stage, rotation)
        image = self.type.inflated_cache.get(cache_key)
        if image is None:
            scale = 1.0 + 1.8 * stage/8
            base = self.type.image
            size = (round(base.get_width()*scale), round(base.get_height()*scale))
            image = pygame.transform.rotate(pygame.transform.smoothscale(base, size), rotation)
            self.type.inflated_cache[cache_key] = image
        self.visual_angle = angle
        self.rotated_image = image
        self.rotated_glow = None

    Animal._update_rotation = bird_rotation
    old_animal_update = Animal.update

    def bird_update(self, player_x, player_y, world):
        if (not getattr(self.type, 'is_dungbird', False)
                or getattr(self, 'is_companion', False)
                or not hasattr(world, 'pea_patches')):
            return old_animal_update(self, player_x, player_y, world)
        self.active = abs(self.x-player_x) <= VIEW_RADIUS+3 and abs(self.y-player_y) <= VIEW_RADIUS+3
        if not self.active or self.hp <= 0:
            return
        patches = world.pea_patches.get(self.home_biome, ())
        target = getattr(self, 'pea_target', None)
        now = pygame.time.get_ticks()
        if (not target or target not in world.flora_grid
                or (world.pea_ready_at.get(target, 0) > now
                    and now >= getattr(self, 'next_pea_search_at', 0))):
            choices = random.sample(patches, min(18, len(patches))) if patches else ()
            ripe = [p for p in choices if world.pea_ready_at.get(p, 0) <= now]
            self.pea_target = min(ripe, key=lambda p: (p[0]+.5-self.x)**2
                                  + (p[1]+.5-self.y)**2, default=None)
            self.next_pea_search_at = now + 1200
            target = self.pea_target
        if target:
            dx, dy = target[0]+0.5-self.x, target[1]+0.5-self.y
            desired = math.atan2(dy, dx) if abs(dx)+abs(dy) > 0.02 else self.swim_heading
        else:
            desired = self.swim_heading + math.sin(now*.00065 + self.swim_phase)*.08
        if (getattr(self, 'poop_charge_started_at', 0) or
                getattr(self, 'poop_burst_remaining', 0)):
            desired = math.atan2(player_y-self.y, player_x-self.x)
        turn = Animal._angle_difference(desired, self.swim_heading)
        self.swim_heading += max(-.16, min(.16, turn))
        speed = self.type.speed * (.28 if (getattr(self, 'poop_charge_started_at', 0) or
                                            getattr(self, 'poop_burst_remaining', 0)) else 1.0)
        nx = self.x + math.cos(self.swim_heading)*speed
        ny = self.y + math.sin(self.swim_heading)*speed
        if self._can_swim_to(nx, ny, world):
            self.x, self.y = nx, ny
        else:
            self.swim_heading += random.choice((-.8, .8))
        self.dx = math.cos(self.swim_heading)*speed
        self.dy = math.sin(self.swim_heading)*speed
        self._update_rotation()

    Animal.update = bird_update

    original_game_init = Game.__init__

    def init_with_dungbirds(self):
        original_game_init(self)
        self.dung_projectiles = []
        self.dung_puddles = []
        self.next_dung_poison_at = 0

    Game.__init__ = init_with_dungbirds
    original_game_update = Game.update

    def update_with_dungbirds(self):
        original_game_update(self)
        if self.state != 'playing' or self.settings_open:
            return
        now = pygame.time.get_ticks()
        world = self.world
        if not self.in_cave:
            for key, ready in list(world.pea_ready_at.items()):
                if now >= ready:
                    if world.flora_grid.get(key) is game_ns['pea_bare_type']:
                        world.flora_grid[key] = game_ns['pea_flora_type']
                    del world.pea_ready_at[key]
            for bird in self.get_nearby_animals(VIEW_RADIUS + 3):
                if (bird.hp <= 0 or not getattr(bird.type, 'is_dungbird', False)
                        or getattr(bird, 'is_companion', False)):
                    if getattr(bird.type, 'is_dungbird', False):
                        bird.poop_burst_remaining = 0
                        bird.poop_charge_started_at = 0
                    continue
                key = getattr(bird, 'pea_target', None)
                if (key is not None and bird.poop_stock_tonnes < DUNGBIRD_MAX_TONNES
                        and now >= getattr(bird, 'pea_next_feed_at', 0)
                        and world.flora_grid.get(key) is game_ns['pea_flora_type']
                        and math.hypot(bird.x - key[0] - .5,
                                       bird.y - key[1] - .5) < .62):
                    # Масса очереди ограничена реально накопленным помётом.
                    bird.poop_stock_tonnes = min(DUNGBIRD_MAX_TONNES,
                                                  bird.poop_stock_tonnes + 1.0)
                    world.flora_grid[key] = game_ns['pea_bare_type']
                    world.pea_ready_at[key] = now + PEA_REGROW_MS
                    bird.pea_next_feed_at = now + 950
                    bird.pea_target = None

                dist = math.hypot(self.player_x - bird.x,
                                  self.player_y - bird.y)
                remaining = getattr(bird, 'poop_burst_remaining', 0)
                if remaining == 0:
                    if (dist <= 6.5 and bird.poop_stock_tonnes >= .01
                            and now >= getattr(bird, 'poop_next_attack_at', 0)
                            and getattr(bird, 'stasis_timer', 0) <= 0):
                        if not getattr(bird, 'poop_charge_started_at', 0):
                            bird.poop_charge_started_at = now or 1
                        elif now - bird.poop_charge_started_at >= 1800:
                            count = random.randint(DUNGBIRD_BURST_MIN,
                                                   DUNGBIRD_BURST_MAX)
                            total_mass = min(bird.poop_stock_tonnes,
                                             random.uniform(.01, DUNGBIRD_MAX_TONNES))
                            total_mass = max(.01, round(total_mass, 3))
                            bird.poop_stock_tonnes = max(
                                0.0, bird.poop_stock_tonnes - total_mass)
                            bird.poop_burst_remaining = count
                            bird.poop_burst_total = count
                            bird.poop_burst_mass_each = total_mass / count
                            bird.poop_burst_next_at = now  # Первый шар — сразу.
                            bird.poop_charge_started_at = 0
                            if dist < VIEW_RADIUS:
                                self.notification = (
                                    f'ГОВНОАТАКА! Очередь {count} шаров, '
                                    f'{total_mass:.2f} т помёта!')
                                self.notification_timer = 105
                    else:
                        bird.poop_charge_started_at = 0

                # Реальное время (миллисекунды), а не счётчик кадров.
                # После начала очередь заканчивается и при отходе игрока.
                if getattr(bird, 'poop_burst_remaining', 0) > 0:
                    if getattr(bird, 'stasis_timer', 0) > 0:
                        bird.poop_burst_next_at = max(
                            bird.poop_burst_next_at, now + DUNGBIRD_SHOT_INTERVAL_MS)
                        continue
                    if (bird.poop_burst_remaining > 0
                            and now >= bird.poop_burst_next_at):
                        direction = math.atan2(self.player_y - bird.y,
                                               self.player_x - bird.x)
                        direction += random.uniform(-DUNGBIRD_SPREAD_RADIANS,
                                                    DUNGBIRD_SPREAD_RADIANS)
                        mass = bird.poop_burst_mass_each
                        # Начальная точка чуть впереди птицы. Запоминаем
                        # исходную клетку: растущий здесь горох не должен
                        # разбить снаряд в момент вылета.
                        start_x = bird.x + math.cos(direction) * .44
                        start_y = bird.y + math.sin(direction) * .44
                        self.dung_projectiles.append({
                            'kind': 'ball', 'x': start_x, 'y': start_y,
                            'vx': math.cos(direction) * DUNGBIRD_SHOT_SPEED,
                            'vy': math.sin(direction) * DUNGBIRD_SHOT_SPEED,
                            'mass': mass,
                            'damage': max(1, round(mass * DUNGBIRD_DAMAGE_PER_TONNE)),
                            'ttl': 60, 'trail': [],
                            'ignore_origin': (int(bird.x), int(bird.y)),
                        })
                        bird.poop_burst_remaining -= 1
                        bird.poop_burst_next_at = now + DUNGBIRD_SHOT_INTERVAL_MS
                        if bird.poop_burst_remaining == 0:
                            bird.poop_next_attack_at = now + 3200
                            bird._update_rotation()
                            break

        def obstacle_at(x, y, projectile=None):
            ix, iy = int(x), int(y)
            if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
                return True
            # Не учитываем исходную клетку шара: птица может стоять
            # непосредственно внутри кустика гороха.
            if (projectile is not None
                    and projectile.get('ignore_origin') == (ix, iy)):
                return False
            obj = world.get_object_at(ix, iy)
            biome = world.get_biome_at(ix, iy)
            return bool(
                (obj is not None and getattr(obj, 'is_wall', False))
                or (ix, iy) in world.flora_grid
                or (ix, iy) in world.tree_grid
                or (biome is not None and not biome.passable)
                or self.is_start_capsule_wall(ix, iy))

        new_projectiles = []
        fragments = []
        for shot in self.dung_projectiles:
            # Не даём тысячам осколков замедлить игру при одновременной атаке стаи.
            if len(new_projectiles) + len(fragments) >= DUNGBIRD_MAX_ACTIVE_PROJECTILES:
                continue
            shot['trail'].append((shot['x'], shot['y']))
            shot['trail'] = shot['trail'][-5 if shot.get('kind') == 'fragment' else -7:]
            old_x, old_y = shot['x'], shot['y']
            new_x = old_x + shot['vx']
            new_y = old_y + shot['vy']
            shot['ttl'] -= 1
            small = shot.get('kind') == 'fragment'
            radius = (max(.055, min(.16, .045 + .09 * shot['mass']**(1/3)))
                      if small else min(.75, .24 + .18 * shot['mass']**(1/3)))
            # Проверяем всю траекторию, чтобы быстрые шары не пролетали
            # сквозь тонкие преграды между кадрами.
            collided = False
            if not self.in_cave:
                for factor in (.33, .66, 1.0):
                    test_x = old_x + shot['vx'] * factor
                    test_y = old_y + shot['vy'] * factor
                    if obstacle_at(test_x, test_y, shot):
                        collided = True
                        break
            out_of_world = not (0 <= new_x < MAP_SIZE and 0 <= new_y < MAP_SIZE)
            if collided:
                if small:
                    if shot.get('bounces_left', 0) > 0:
                        # Отражение скорости от клетки + угловой разброс.
                        flip_x = obstacle_at(new_x, old_y, shot)
                        flip_y = obstacle_at(old_x, new_y, shot)
                        if flip_x or (not flip_x and not flip_y):
                            shot['vx'] = -shot['vx']
                        if flip_y or (not flip_x and not flip_y):
                            shot['vy'] = -shot['vy']
                        perturb = random.uniform(-.32, .32)
                        cs, sn = math.cos(perturb), math.sin(perturb)
                        vx, vy = shot['vx'], shot['vy']
                        shot['vx'] = (vx * cs - vy * sn) * .88
                        shot['vy'] = (vx * sn + vy * cs) * .88
                        shot['bounces_left'] -= 1
                        shot['x'], shot['y'] = old_x, old_y
                        if shot['ttl'] > 0:
                            new_projectiles.append(shot)
                    continue
                # Один большой шар разбивается на 3–7 маленьких частиц.
                count = random.randint(DUNGBIRD_FRAGMENT_MIN,
                                       DUNGBIRD_FRAGMENT_MAX)
                reflection = math.atan2(-shot['vy'], -shot['vx'])
                for index in range(count):
                    if len(new_projectiles) + len(fragments) >= DUNGBIRD_MAX_ACTIVE_PROJECTILES:
                        break
                    scatter = ((index - (count - 1) / 2) /
                               max(1, (count - 1) / 2)) * 1.33
                    angle = reflection + scatter + random.uniform(-.30, .30)
                    speed = random.uniform(.11, .24)
                    fragments.append({
                        'kind': 'fragment', 'x': old_x, 'y': old_y,
                        'vx': math.cos(angle) * speed,
                        'vy': math.sin(angle) * speed,
                        'mass': shot['mass'] / count,
                        'damage': max(1, round(shot['damage'] * .2 / count)),
                        'ttl': random.randint(14, 34), 'trail': [],
                        'bounces_left': random.randint(1, 2),
                        'ignore_origin': (int(old_x), int(old_y)),
                    })
                self.dung_puddles.append({
                    'x': old_x, 'y': old_y, 'radius': radius * .55,
                    'until': now + 7000})
                continue
            if out_of_world:
                continue
            if not self.in_cave and math.hypot(new_x - self.player_x,
                                              new_y - self.player_y) < radius + .34:
                self._damage_player_or_vehicle(shot['damage'])
                if not small:
                    self.dung_puddles.append({
                        'x': new_x, 'y': new_y, 'radius': radius + .42,
                        'until': now + 10000})
                self.notification = f'Попадание помёта: -{shot["damage"]} HP!'
                self.notification_timer = 90
                continue
            if shot['ttl'] <= 0:
                if not small:
                    self.dung_puddles.append({
                        'x': new_x, 'y': new_y, 'radius': radius + .42,
                        'until': now + 10000})
                continue
            shot['x'], shot['y'] = new_x, new_y
            new_projectiles.append(shot)
        self.dung_projectiles = (new_projectiles + fragments)[
            -DUNGBIRD_MAX_ACTIVE_PROJECTILES:]
        self.dung_puddles = [p for p in self.dung_puddles
                             if p['until'] > now][-250:]
        if not self.in_cave and now >= self.next_dung_poison_at:
            for puddle in self.dung_puddles:
                if math.hypot(self.player_x - puddle['x'],
                              self.player_y - puddle['y']) < puddle['radius']:
                    self._damage_player_or_vehicle(3)
                    self.next_dung_poison_at = now + 1100
                    break

    Game.update = update_with_dungbirds
    original_pick_up = Game.pick_up

    def pick_up_with_peas(self):
        if not self.in_cave:
            key = (int(self.player_x), int(self.player_y))
            if (self.world.flora_grid.get(key) is game_ns['pea_flora_type']
                    and not self.world.get_object_at(*key)
                    and not self._current_dropped_items().get(key)
                    and not self._moss_near(self.player_x, self.player_y, .72)
                    and not any(int(a.x) == key[0] and int(a.y) == key[1]
                                for a in self.get_nearby_animals(1.5))):
                if self.add_to_inventory(game_ns['pea_pod_type']):
                    self.world.flora_grid[key] = game_ns['pea_bare_type']
                    self.world.pea_ready_at[key] = pygame.time.get_ticks()+PEA_REGROW_MS
                    self.notification = 'Собран стручок гороха! (E, затем R/U — съесть)'
                else:
                    self.notification = 'Инвентарь полон, горох не собран'
                self.notification_timer = 95
                return
        return original_pick_up(self)

    Game.pick_up = pick_up_with_peas
    original_scanner_details = Game._scanner_details

    def scanner_details_with_dungbirds(self, target):
        entity = target['entity']
        if entity is game_ns.get('dungbird_type'):
            return [
                'Вид: Avis pachomica (Птица Говносрун).',
                'Обычная длина: 25 см; перед атакой раздувается до нескольких метров.',
                'Здоровье: 25 HP. Скорость: 4 клетки/сек.',
                'Питание: горох и другие плоды семейства Бобовые.',
                '1 г съеденного гороха превращается в 1 тонну токсичного помёта.',
                'Очередь: 10–30 шаров, интервал 0,5 секунды, разброс при стрельбе.',
                'Масса всей очереди: от 10 кг до 10 т; 100 HP за тонну.',
                'При попадании в стену или растение шар разбивается на 3–7',
                'осколков, которые отскакивают в разные стороны.',
                'После попадания остаётся токсичная лужа.',
                'Распространение: леса, побережья, острова и горы.',
                'Совет КПК: при раздувании птицы немедленно уходите с линии огня.'
            ]
        if getattr(entity, 'kind', None) == 'pea':
            return [
                'Вид: горох, семейство Бобовые.',
                'Среда обитания: наземные биомы.',
                'На кусте растут зелёные стручки; E — собрать стоя на растении.',
                'Стручки съедобны: +6 еды, +2 воды.',
                'После сбора новый урожай вырастает за 25 секунд.',
                'Любимый корм Avis pachomica: 1 горошина (1 г) = 1 т помёта.',
                'Исследованный горох можно посадить в грядке или горшке.'
            ]
        return original_scanner_details(self, target)

    Game._scanner_details = scanner_details_with_dungbirds
    original_render = Game.render

    def render_with_dungbirds(self):
        original_render(self)
        if self.state != 'playing' or self.in_cave:
            return
        screen = pygame.display.get_surface()
        zoom = self.camera_zoom

        def pos(x, y):
            return (round(SCREEN_SIZE*.5+(x-self.player_x)*CELL_SIZE*zoom),
                    round(SCREEN_SIZE*.5+(y-self.player_y)*CELL_SIZE*zoom))

        now = pygame.time.get_ticks()
        overlay = pygame.Surface(screen.get_size(), pygame.SRCALPHA)
        for puddle in self.dung_puddles:
            sx, sy = pos(puddle['x'], puddle['y'])
            radius = round(puddle['radius']*CELL_SIZE*zoom)
            # В режиме паузы/меню время продолжает идти, а очистка луж
            # может не выполняться. Просроченную лужу нельзя рисовать:
            # отрицательная прозрачность даёт ValueError: invalid color argument.
            remaining_ms = puddle['until'] - now
            if remaining_ms <= 0:
                continue
            if -radius < sx < SCREEN_SIZE+radius and -radius < sy < SCREEN_SIZE+radius:
                alpha = max(0, min(170, round(170 * remaining_ms / 10000)))
                pygame.draw.circle(overlay, (79, 61, 26, alpha), (sx, sy), max(3, radius))
                pygame.draw.circle(overlay, (143, 170, 49, min(210, alpha + 30)),
                                   (sx, sy), max(2, round(radius*.68)), max(2, round(3*zoom)))
        for shot in self.dung_projectiles:
            sx, sy = pos(shot['x'], shot['y'])
            if not (-100 < sx < SCREEN_SIZE + 100 and
                    -100 < sy < SCREEN_SIZE + 100):
                continue
            small = shot.get('kind') == 'fragment'
            radius = (max(2, round((.05 + .045 * shot['mass']**(1/3)) *
                                    CELL_SIZE * zoom)) if small else
                      max(4, round((.16 + .17 * shot['mass']**(1/3)) *
                                    CELL_SIZE * zoom)))
            for i, (px, py) in enumerate(shot['trail']):
                trail_pos = pos(px, py)
                pygame.draw.circle(overlay,
                                   (174, 137, 61, min(255, 30 + i * 22)) if small
                                   else (141, 97, 47, min(255, 35 + i * 22)),
                                   trail_pos, max(1, round(radius * (i+1) / 10)))
            pygame.draw.circle(overlay, (60, 45, 22, 245),
                               (sx, sy), radius + (1 if small else 2))
            pygame.draw.circle(overlay,
                               (176, 140, 71, 255) if small
                               else (139, 100, 52, 255),
                               (sx, sy), radius)
            if not small:
                pygame.draw.circle(overlay, (196, 165, 83, 255),
                                   (sx - radius//3, sy - radius//3),
                                   max(2, radius//3))
        screen.blit(overlay, (0, 0))
        for bird in self.get_nearby_animals(VIEW_RADIUS+2):
            if bird.hp <= 0 or not getattr(bird.type, 'is_dungbird', False):
                continue
            started = getattr(bird, 'poop_charge_started_at', 0)
            bursting = getattr(bird, 'poop_burst_remaining', 0)
            if started or bursting:
                sx, sy = pos(bird.x, bird.y)
                progress = (1.0 if bursting else min(1.0, (now-started)/1800))
                ring_radius = round((.65+1.25*progress)*CELL_SIZE*zoom)
                pygame.draw.circle(screen, (241, 184, 41), (sx, sy), max(3,ring_radius), 2)
                caption = (f'ЗАЛП: {bursting} шаров' if bursting else
                           f'ПОМЁТ: {bird.poop_stock_tonnes:.1f} т')
                text_image = self.tiny_font.render(caption,
                                                    True, (252, 226, 97))
                screen.blit(text_image, text_image.get_rect(center=(sx, sy-ring_radius-12)))
        pygame.display.flip()

    Game.render = render_with_dungbirds


install_dungbird_and_peas(globals())
# <<< AVIS_PACHOMICA_MOD_V1 <<<


# >>> AKLASUR_MOD_V1: Акласур — рыба, которая вышла на сушу >>>
AKLASUR_NAME = 'АКЛАСУР'
AKLASUR_HEIGHT_METERS = 1.5
AKLASUR_HP = 100
AKLASUR_SPEED = 1.0 / 60.0  # 1 игровая клетка/сек при 60 FPS
AKLASUR_BITE_DAMAGE = 5
AKLASUR_KICK_DAMAGE = 20


def generate_aklasur_sprite(stride=0, size=76):
    """Повернутый вправо рыбочеловек наоборот: рыбья голова и тело, человеческие ноги."""
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    scale = size / 80.0

    def p(x, y):
        return (round(x * scale), round(y * scale))

    def shape(points, color):
        pygame.draw.polygon(surf, color, [p(*xy) for xy in points])

    def oval(x, y, w, h, color):
        pygame.draw.ellipse(surf, color, pygame.Rect(round(x*scale), round(y*scale),
                                                      max(1, round(w*scale)), max(1, round(h*scale))))

    # Две человеческие ноги с коленями, стопами и пальцами — не рыбьи плавники.
    offset = 5 if stride else -5
    for upper, foot in [((25, 30), (4, 12+offset)), ((27, 50), (4, 65-offset))]:
        knee = (14, upper[1] + (3 if upper[1] < 40 else -3))
        pygame.draw.line(surf, (93, 58, 44), p(*upper), p(*knee), max(8, round(12*scale)))
        pygame.draw.line(surf, (211, 151, 112), p(*upper), p(*knee), max(5, round(9*scale)))
        pygame.draw.line(surf, (93, 58, 44), p(*knee), p(*foot), max(7, round(10*scale)))
        pygame.draw.line(surf, (226, 169, 129), p(*knee), p(*foot), max(5, round(7*scale)))
        fx, fy = foot
        oval(fx-2, fy-4, 14, 9, (219, 164, 127))
        for j in range(3):
            pygame.draw.circle(surf, (239, 187, 147), p(fx+9+j, fy-2+j*2), max(1, round(1.7*scale)))

    # Рыбье тело занимает верхнюю часть силуэта; ноги начинаются за брюшком.
    shape([(19, 40), (29, 23), (43, 19), (58, 22), (69, 30),
           (72, 42), (65, 51), (52, 58), (38, 59), (25, 52)], (30, 86, 116))
    shape([(25, 39), (39, 30), (53, 28), (67, 35), (70, 44),
           (57, 52), (39, 55), (27, 49)], (72, 158, 171))
    shape([(30, 26), (39, 13), (49, 17), (55, 27)], (26, 84, 107))
    shape([(27, 51), (40, 66), (52, 56), (46, 52)], (24, 102, 131))
    # Чешуя, жаберные крышки и крупная рыбья голова с раскрытым ртом.
    for x, y in ((33, 34), (41, 41), (36, 49), (49, 34), (49, 48)):
        pygame.draw.arc(surf, (145, 208, 202), pygame.Rect(*p(x-4, y-4),
                         max(3, round(8*scale)), max(3, round(8*scale))),
                         -0.9, 1.2, max(1, round(scale)))
    oval(56, 29, 15, 23, (106, 185, 186))
    pygame.draw.arc(surf, (26, 75, 92), pygame.Rect(*p(54, 31),
                    max(2, round(12*scale)), max(2, round(20*scale))),
                    1.6, 4.2, max(1, round(2*scale)))
    oval(61, 32, 7, 7, (247, 235, 181))
    pygame.draw.circle(surf, (16, 25, 26), p(65, 35), max(1, round(2.5*scale)))
    shape([(69, 42), (79, 45), (71, 50), (67, 47)], (32, 66, 80))
    pygame.draw.line(surf, (234, 168, 147), p(71, 48), p(77, 46), max(1, round(2*scale)))
    return surf


def install_aklasur(game_ns):
    if game_ns.get('_AKLASUR_INSTALLED'):
        return
    game_ns['_AKLASUR_INSTALLED'] = True
    old_init_assets = game_ns['init_assets']

    def init_assets_with_aklasur():
        old_init_assets()
        aklasur = AnimalType(AKLASUR_NAME, (64, 151, 168), 42,
                             speed=AKLASUR_SPEED, behavior='neutral',
                             hp=AKLASUR_HP, damage=0, edible=False,
                             volume=3)
        aklasur.is_aklasur = True
        aklasur.fish_style = True
        aklasur.image = generate_aklasur_sprite(0)
        aklasur.glow_image = None
        aklasur.walk_frames = (aklasur.image, generate_aklasur_sprite(1))
        aklasur.walk_rotated_cache = {}
        upper = ObjectType('Верхняя половина Акласура', (88, 159, 167), 30,
                           edible=True, hunger_value=48, thirst_value=5, volume=2)
        upper.image = pygame.transform.smoothscale(aklasur.image, (36, 36))
        legs = ObjectType('Ноги Акласура (несъедобные)', (219, 161, 121), 27,
                          edible=False, volume=1)
        legs.image = pygame.Surface((32, 32), pygame.SRCALPHA)
        for y, delta in ((11, -4), (23, 4)):
            pygame.draw.line(legs.image, (219, 161, 121), (29, y), (9, y+delta), 7)
            pygame.draw.ellipse(legs.image, (230, 178, 138), (1, y+delta-4, 12, 9))
        game_ns['aklasur_type'] = aklasur
        game_ns['aklasur_upper_type'] = upper
        game_ns['aklasur_legs_type'] = legs

    game_ns['init_assets'] = init_assets_with_aklasur

    old_generate_flora = World._generate_flora

    def generate_flora_and_aklasurs(self):
        old_generate_flora(self)
        rng = random.Random(2026102507)
        aklasur_type = game_ns['aklasur_type']
        for biome in self.biomes:
            if not biome.passable or biome.name not in ('Побережье', 'Острова'):
                continue
            quantity = min(5, max(1, round(biome.area()/10000)))
            for _ in range(quantity):
                chosen = None
                fallback = None
                for _attempt in range(120):
                    rx, ry = biome.random_point(rng)
                    x, y = int(rx), int(ry)
                    if self.get_biome_at(x, y) is not biome:
                        continue
                    obj = self.get_object_at(x, y)
                    if obj is not None and obj.is_wall:
                        continue
                    fallback = (rx, ry)
                    if any(self.is_water_at(x+dx, y+dy)
                           for dx, dy in ((6, 0), (-6, 0), (0, 6), (0, -6),
                                          (12, 0), (-12, 0), (0, 12), (0, -12))):
                        chosen = (rx, ry)
                        break
                if chosen is None:
                    chosen = fallback
                if chosen is not None:
                    animal = Animal(*chosen, aklasur_type, biome)
                    animal.aklasur_attack_is_kick = False
                    animal.aklasur_attack_ready_at = 0
                    animal.aklasur_attack_flash_until = 0
                    self.animals.append(animal)

    World._generate_flora = generate_flora_and_aklasurs

    old_can_swim_to = Animal._can_swim_to

    def aklasur_can_move_to(self, x, y, world):
        if not getattr(self.type, 'is_aklasur', False):
            return old_can_swim_to(self, x, y, world)
        if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
            return False
        if hasattr(world, 'cave_lookup'):
            return False
        biome = world.get_biome_at(x, y)
        if biome is None or not biome.passable:
            return False
        if biome.name not in LAND_BIOMES and biome.name not in ('Мелководье', 'Реки', 'Притоки'):
            return False
        obj = world.get_object_at(int(x), int(y))
        if obj is not None and obj.is_wall:
            return False
        blocked = getattr(world, 'creature_blocked', None)
        return blocked is None or not blocked(int(x), int(y))

    Animal._can_swim_to = aklasur_can_move_to
    old_rotation = Animal._update_rotation

    def aklasur_rotation(self):
        if not getattr(self.type, 'is_aklasur', False):
            return old_rotation(self)
        angle = -math.degrees(math.atan2(self.dy, self.dx)) if self.dx or self.dy else 0
        moving = abs(self.dx)+abs(self.dy) > .003
        stride = int(pygame.time.get_ticks()/220) % 2 if moving else 0
        degrees = round(angle/15)*15
        key = (stride, degrees)
        cached = self.type.walk_rotated_cache.get(key)
        if cached is None:
            cached = pygame.transform.rotate(self.type.walk_frames[stride], degrees)
            self.type.walk_rotated_cache[key] = cached
        self.visual_angle = angle
        self.rotated_image = cached
        self.rotated_glow = None

    Animal._update_rotation = aklasur_rotation
    old_animal_update = Animal.update

    def aklasur_update(self, player_x, player_y, world):
        if (not getattr(self.type, 'is_aklasur', False)
                or getattr(self, 'is_companion', False)):
            return old_animal_update(self, player_x, player_y, world)
        self.active = abs(self.x-player_x) <= VIEW_RADIUS+2 and abs(self.y-player_y) <= VIEW_RADIUS+2
        if not self.active or self.hp <= 0:
            return
        dist = math.hypot(player_x-self.x, player_y-self.y)
        if .4 < dist < 6.0:
            target_angle = math.atan2(player_y-self.y, player_x-self.x)
        else:
            self.swim_phase += .035
            target_angle = self.cruise_heading + .4*math.sin(self.swim_phase)
            if random.random() < .007:
                self.cruise_heading += random.uniform(-.8, .8)
        turn = Animal._angle_difference(target_angle, self.swim_heading)
        self.swim_heading += max(-.13, min(.13, turn))
        velocity = self.type.speed
        nx = self.x+math.cos(self.swim_heading)*velocity
        ny = self.y+math.sin(self.swim_heading)*velocity
        if self._can_swim_to(nx, ny, world):
            self.x, self.y = nx, ny
        else:
            self.swim_heading += random.choice((-.65, .65))
            self.cruise_heading = self.swim_heading
        self.dx = math.cos(self.swim_heading)*velocity
        self.dy = math.sin(self.swim_heading)*velocity
        self._update_rotation()

    Animal.update = aklasur_update

    old_game_update = Game.update

    def update_with_aklasurs(self):
        old_game_update(self)
        if self.state != 'playing' or self.settings_open or self.in_cave:
            return
        now = pygame.time.get_ticks()
        for animal in self.get_nearby_animals(2.2):
            if (animal.hp <= 0 or not getattr(animal.type, 'is_aklasur', False)
                    or getattr(animal, 'is_companion', False)
                    or getattr(animal, 'stasis_timer', 0) > 0
                    or now < getattr(animal, 'aklasur_attack_ready_at', 0)):
                continue
            distance = math.hypot(animal.x-self.player_x, animal.y-self.player_y)
            if distance > 1.5:
                continue
            kick = getattr(animal, 'aklasur_attack_is_kick', False)
            # Чередование приёмов: кусает за 5 HP, затем бьёт ногой за 20 HP.
            damage = AKLASUR_KICK_DAMAGE if kick else AKLASUR_BITE_DAMAGE
            animal.aklasur_attack_is_kick = not kick
            animal.aklasur_attack_ready_at = now + (1700 if kick else 1100)
            animal.aklasur_attack_flash_until = now + 380
            animal.aklasur_last_attack = 'УДАР НОГОЙ' if kick else 'УКУС'
            self._damage_player_or_vehicle(damage)
            if self.running:
                self.notification = f'{AKLASUR_NAME}: {animal.aklasur_last_attack}! −{damage} HP'
                self.notification_timer = 90

    Game.update = update_with_aklasurs
    old_pick_up = Game.pick_up

    def pickup_without_aklasur_legs(self):
        if not self.in_cave:
            px, py = int(self.player_x), int(self.player_y)
            for animal in self.get_nearby_animals(1.5):
                if (getattr(animal.type, 'is_aklasur', False)
                        and int(animal.x) == px and int(animal.y) == py):
                    if animal.hp <= 0:
                        self.handle_animal_death(animal)
                    else:
                        self.notification = 'Живой Акласур не даёт себя подобрать!'
                        self.notification_timer = 75
                    return
        return old_pick_up(self)

    Game.pick_up = pickup_without_aklasur_legs
    old_death = Game.handle_animal_death

    def aklasur_death(self, animal):
        if not getattr(animal.type, 'is_aklasur', False):
            return old_death(self, animal)
        # Ноги намеренно несъедобны; в инвентарь как пища попадает только рыба.
        drops = self._current_dropped_items()
        pos = (int(animal.x), int(animal.y))
        upper = game_ns['aklasur_upper_type']
        legs = game_ns['aklasur_legs_type']
        collected = self.add_to_inventory(upper)
        if not collected:
            drops.setdefault(pos, []).append(upper)
        drops.setdefault(pos, []).append(legs)
        self.remove_current_animal(animal)
        self.notification = ('Акласур разделан: съедобная верхняя половина получена, '
                             'ноги остались на земле.' if collected else
                             'Акласур погиб: верхняя половина и несъедобные ноги остались на земле.')
        self.notification_timer = 130

    Game.handle_animal_death = aklasur_death
    old_scanner_details = Game._scanner_details

    def scanner_details_with_aklasur(self, target):
        entity = target['entity']
        if entity is game_ns.get('aklasur_type'):
            return [
                f'Вид: {AKLASUR_NAME}. Рыбья голова, рыбье туловище и человеческие ноги.',
                'Первое животное Абсероида, вышедшее из древнего океана на сушу.',
                'Рост: 1,5 м. Здоровье: 100 HP. Скорость: 1 клетка/сек.',
                'Атаки: укус — 5 HP; удар ногой — 20 HP.',
                'Обитает на побережьях и островах, может входить в мелководье.',
                'Съедобна только верхняя, рыбья половина тела.',
                'При гибели разделяется на съедобную верхнюю часть и несъедобные ноги.',
                'Совет КПК: особенно опасайтесь удара ногой.'
            ]
        return old_scanner_details(self, target)

    Game._scanner_details = scanner_details_with_aklasur
    old_render = Game.render

    def render_with_aklasurs(self):
        old_render(self)
        if self.state != 'playing' or self.in_cave:
            return
        display = pygame.display.get_surface()
        now = pygame.time.get_ticks()
        zoom = self.camera_zoom
        for animal in self.get_nearby_animals(VIEW_RADIUS+2):
            if not getattr(animal.type, 'is_aklasur', False) or animal.hp <= 0:
                continue
            until = getattr(animal, 'aklasur_attack_flash_until', 0)
            if until <= now:
                continue
            sx = round(SCREEN_SIZE*.5+(animal.x-self.player_x)*CELL_SIZE*zoom)
            sy = round(SCREEN_SIZE*.5+(animal.y-self.player_y)*CELL_SIZE*zoom)
            radius = round(34*zoom)
            pygame.draw.circle(display, (250, 100, 70), (sx, sy), radius, 3)
            label = self.small_font.render(getattr(animal, 'aklasur_last_attack', 'АТАКА'),
                                           True, (255, 229, 151))
            display.blit(label, label.get_rect(center=(sx, sy-radius-12)))
        pygame.display.flip()

    Game.render = render_with_aklasurs


install_aklasur(globals())
# <<< AKLASUR_MOD_V1 <<<


# >>> LEECH_HYDRA_MOD_V1: Пиявка-гидра >>>
HYDRA_NAME = 'Пиявка-гидра'
HYDRA_BODY_HP = 70
HYDRA_TENTACLE_HP = 9
HYDRA_BODY_METERS = 1.2
HYDRA_REACH_METERS = 4.0
HYDRA_DAMAGE_PER_TENTACLE_SECOND = 1.0
HYDRA_REGEN_MS = 26000


def generate_leech_hydra_body_sprite(size=38):
    """На поверхности виден лишь скрытый в грунте массивный тёмный хоботок."""
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    d = pygame.draw
    cx = cy = size//2
    d.ellipse(surf, (28, 57, 61, 145), (2, 8, size-4, size-14))
    d.ellipse(surf, (20, 41, 45, 235), (6, 11, size-12, size-19))
    d.ellipse(surf, (49, 76, 65, 255), (10, 13, size-20, size-24))
    d.ellipse(surf, (12, 21, 22, 255), (cx-7, cy-5, 14, 10))
    d.circle(surf, (137, 162, 110), (cx-4, cy-1), 2)
    d.circle(surf, (137, 162, 110), (cx+4, cy-1), 2)
    for angle in range(0, 360, 45):
        a = math.radians(angle)
        p1 = (round(cx+12*math.cos(a)), round(cy+9*math.sin(a)))
        p2 = (round(cx+18*math.cos(a)), round(cy+13*math.sin(a)))
        d.line(surf, (28, 45, 41, 185), p1, p2, 2)
    return surf


def _hydra_initialize_tentacles(animal):
    """Каждое щупальце имеет собственные 9 HP, состояние и время регенерации."""
    rng = random.Random(740003+int(animal.x*131)+int(animal.y*317))
    count = rng.randint(10, 15)
    animal.hydra_tentacles = []
    for index in range(count):
        animal.hydra_tentacles.append({
            'angle': math.tau*index/count+rng.uniform(-.11, .11),
            'length': rng.uniform(2.5, HYDRA_REACH_METERS),
            'phase': rng.uniform(0, math.tau),
            'hp': HYDRA_TENTACLE_HP,
            'regrow_at': 0,
            'attached': False,
            'target': None,          # 'player' или конкретная рыба
            'blood_carry': 0.0,      # дробная часть HP, высосанная этим щупальцем
        })
    animal.hydra_next_attach_at = 0
    animal.hydra_damage_carry = 0.0
    animal.hydra_last_tick = pygame.time.get_ticks()
    animal.hydra_last_damage_at = 0
    animal.hydra_blood_drunk = 0


def _hydra_tentacle_tip(animal, tentacle, now, player_xy=None):
    if tentacle['attached']:
        target = tentacle.get('target')
        if target == 'player' and player_xy is not None:
            return (float(player_xy[0]), float(player_xy[1]))
        if isinstance(target, Animal) and target.hp > 0:
            return (float(target.x), float(target.y))
    angle = tentacle['angle']+.12*math.sin(now*.0014+tentacle['phase'])
    radius = tentacle['length']*(.92+.08*math.sin(now*.002+tentacle['phase']))
    return (animal.x+math.cos(angle)*radius,
            animal.y+math.sin(angle)*radius)


def _hydra_is_fish(animal):
    """Все плавниковые рыбы, включая добавленные модами; без птиц и наземных видов."""
    if animal.hp <= 0:
        return False
    kind = animal.type
    if (getattr(kind, 'is_leech_hydra', False)
            or getattr(kind, 'is_dungbird', False)
            or getattr(kind, 'is_aklasur', False)
            or kind.name == WHALE_NAME):
        return False
    return (bool(getattr(kind, 'fish_style', False) or getattr(kind, 'schooling', False))
            and not is_land_biome(animal.home_biome))


def _hydra_segment_distance_sq(x, y, ax, ay, bx, by):
    vx, vy = bx-ax, by-ay
    length_sq = vx*vx+vy*vy
    if length_sq <= 1e-10:
        return (x-ax)**2+(y-ay)**2
    t = max(0.0, min(1.0, ((x-ax)*vx+(y-ay)*vy)/length_sq))
    cx, cy = ax+t*vx, ay+t*vy
    return (x-cx)**2+(y-cy)**2


def install_leech_hydra(game_ns):
    if game_ns.get('_LEECH_HYDRA_INSTALLED'):
        return
    game_ns['_LEECH_HYDRA_INSTALLED'] = True
    old_init_assets = game_ns['init_assets']

    def init_assets_with_hydra():
        old_init_assets()
        hydra = AnimalType(HYDRA_NAME, (45, 75, 64), 38,
                           speed=0.0, behavior='neutral', hp=HYDRA_BODY_HP,
                           damage=0, edible=True, hunger_value=32,
                           thirst_value=0, volume=3)
        hydra.is_leech_hydra = True
        hydra.image = generate_leech_hydra_body_sprite(38)
        hydra.glow_image = None
        game_ns['leech_hydra_type'] = hydra

    game_ns['init_assets'] = init_assets_with_hydra
    old_generate_animals = World._generate_animals

    def generate_animals_with_hydras(self):
        old_generate_animals(self)
        rng = random.Random(2026102591)
        hydra_type = game_ns['leech_hydra_type']
        for biome in self.biomes:
            if biome.name != 'Мелководье' or not biome.passable:
                continue
            # Несколько засад на целый биом, а не плотная популяция.
            quantity = min(3, max(1, round(biome.area()/26000)))
            for _ in range(quantity):
                for _attempt in range(100):
                    x, y = biome.random_point(rng)
                    if self.get_biome_at(x, y) is not biome:
                        continue
                    obj = self.get_object_at(int(x), int(y))
                    if obj is not None and obj.is_wall:
                        continue
                    animal = Animal(x, y, hydra_type, biome)
                    _hydra_initialize_tentacles(animal)
                    self.animals.append(animal)
                    break

    World._generate_animals = generate_animals_with_hydras
    old_animal_update = Animal.update

    def hydra_animal_update(self, player_x, player_y, world):
        if not getattr(self.type, 'is_leech_hydra', False):
            return old_animal_update(self, player_x, player_y, world)
        self.active = abs(self.x-player_x) <= VIEW_RADIUS+4 and abs(self.y-player_y) <= VIEW_RADIUS+4
        # Тело закопано: без движения и без стандартного контактного урона.
        self.dx = self.dy = 0.0
        return

    Animal.update = hydra_animal_update
    old_spawn_animal = Game._spawn_animal_from_inventory_item

    def spawn_hydra_with_tentacles(self, animal_type, companion=False):
        animal = old_spawn_animal(self, animal_type, companion=companion)
        if animal is not None and getattr(animal.type, 'is_leech_hydra', False):
            _hydra_initialize_tentacles(animal)
            animal.companion_state = ('засела и ловит рыбу' if companion
                                      else 'подстерегает добычу')
        return animal

    Game._spawn_animal_from_inventory_item = spawn_hydra_with_tentacles
    old_update_companion = Game._update_companion

    def update_hydra_companion(self, animal, player_move_x, player_move_y):
        if not getattr(animal.type, 'is_leech_hydra', False):
            return old_update_companion(self, animal, player_move_x, player_move_y)
        # Ручная гидра — стационарная ловушка, не плавает вокруг хозяина.
        animal.active = (abs(animal.x-self.player_x) <= VIEW_RADIUS+4
                         and abs(animal.y-self.player_y) <= VIEW_RADIUS+4)
        animal.dx = animal.dy = 0.0
        catches = any(arm['attached'] and isinstance(arm.get('target'), Animal)
                      for arm in getattr(animal, 'hydra_tentacles', ()))
        animal.companion_state = ('пьёт кровь пойманной рыбы' if catches
                                  else 'ловит рыбу щупальцами')

    Game._update_companion = update_hydra_companion
    old_game_update = Game.update

    def update_with_hydras(self):
        old_game_update(self)
        if self.state != 'playing' or self.settings_open or self.in_cave:
            return
        now = pygame.time.get_ticks()
        for hydra in self.get_nearby_animals(6.0):
            if hydra.hp <= 0 or not getattr(hydra.type, 'is_leech_hydra', False):
                continue
            if not hasattr(hydra, 'hydra_tentacles'):
                _hydra_initialize_tentacles(hydra)
            elapsed_ms = max(0, now-hydra.hydra_last_tick)
            hydra.hydra_last_tick = now
            # Долгая пауза или отсутствие игрока не вызывает накопленный урон.
            if elapsed_ms > 1100:
                for arm in hydra.hydra_tentacles:
                    arm['attached'] = False
                    arm['target'] = None
                    arm['blood_carry'] = 0.0
                hydra.hydra_damage_carry = 0.0
                elapsed_ms = 0
            for arm in hydra.hydra_tentacles:
                if arm['hp'] <= 0 and now >= arm['regrow_at']:
                    arm['hp'] = HYDRA_TENTACLE_HP
                    arm['regrow_at'] = 0
                    arm['attached'] = False
                    arm['target'] = None
                    arm['blood_carry'] = 0.0

            # Все рыбьи виды, включая добавленные модами: даже те, что
            # не являются обычными съедобными стайными рыбами.
            fishes = [fish for fish in self.world.get_animals_near(
                hydra.x, hydra.y, HYDRA_REACH_METERS+1)
                if fish is not hydra and _hydra_is_fish(fish)
                and (not hydra.is_companion or not fish.is_companion)
                and math.hypot(fish.x-hydra.x, fish.y-hydra.y) <= HYDRA_REACH_METERS]
            live_fish = set(fishes)
            player_distance = math.hypot(hydra.x-self.player_x,
                                         hydra.y-self.player_y)

            # Живая добыча может уплыть, а игрок — уйти из радиуса сети.
            # Хозяин никогда не входит в список целей ручной гидры.
            for arm in hydra.hydra_tentacles:
                target = arm.get('target')
                if target == 'player':
                    valid = (not hydra.is_companion
                             and player_distance <= arm['length'])
                elif isinstance(target, Animal):
                    valid = (target in live_fish and
                             math.hypot(target.x-hydra.x,
                                        target.y-hydra.y) <= arm['length'])
                else:
                    valid = False
                if arm['hp'] <= 0 or not valid or getattr(hydra, 'stasis_timer', 0) > 0:
                    arm['attached'] = False
                    arm['target'] = None
                    arm['blood_carry'] = 0.0
            if getattr(hydra, 'stasis_timer', 0) > 0:
                continue

            if now >= hydra.hydra_next_attach_at:
                free_arms = [arm for arm in hydra.hydra_tentacles
                             if arm['hp'] > 0 and not arm['attached']]
                targets = list(fishes)
                if not hydra.is_companion and player_distance <= HYDRA_REACH_METERS:
                    targets.append('player')
                for arm in free_arms:
                    reachable = []
                    for target in targets:
                        d = (player_distance if target == 'player' else
                             math.hypot(target.x-hydra.x, target.y-hydra.y))
                        if d <= arm['length']:
                            attached_count = sum(a['attached'] and a.get('target') == target
                                                 for a in hydra.hydra_tentacles)
                            # Распределяем щупальца между рыбами; при прочих
                            # равных сначала выбираем рыбу, затем игрока.
                            reachable.append((attached_count,
                                              1 if target == 'player' else 0,
                                              d, target))
                    if reachable:
                        _, _, _, target = min(reachable, key=lambda item: item[:3])
                        arm['attached'] = True
                        arm['target'] = target
                        arm['blood_carry'] = 0.0
                        hydra.hydra_next_attach_at = now+550
                        if target == 'player':
                            attached = sum(a['attached'] and a.get('target') == 'player'
                                           for a in hydra.hydra_tentacles)
                            self.notification = f'Пиявка-гидра: присосалось щупалец {attached}!'
                            self.notification_timer = 48
                        break

            player_damage = 0
            fish_damage = {}
            for arm in hydra.hydra_tentacles:
                if not arm['attached'] or arm['hp'] <= 0:
                    continue
                # Каждая присоска отдельно копит 1 HP/сек на своей жертве.
                arm['blood_carry'] += elapsed_ms/1000.0 * HYDRA_DAMAGE_PER_TENTACLE_SECOND
                damage = int(arm['blood_carry'])
                if damage <= 0:
                    continue
                arm['blood_carry'] -= damage
                target = arm['target']
                if target == 'player' and not hydra.is_companion:
                    player_damage += damage
                elif isinstance(target, Animal) and target in live_fish:
                    fish_damage[target] = fish_damage.get(target, 0) + damage
            if player_damage:
                self._damage_player_or_vehicle(player_damage)
                hydra.hydra_last_damage_at = now
            for fish, damage in fish_damage.items():
                if fish.hp <= 0:
                    continue
                old_hp = fish.hp
                # Даже прикрученных рыб-крепежей присоска достаёт изнутри:
                # временно снимаем защиту от обычных ударов только на момент
                # кровососания, не откручивая рыбу от её опоры.
                anchored_fastener = (getattr(fish, 'fastener_anchored', False)
                                     and getattr(fish.type, 'fastener_kind', None))
                if anchored_fastener:
                    fish.fastener_anchored = False
                try:
                    fish.hp = max(0, fish.hp-damage)
                finally:
                    if anchored_fastener:
                        fish.fastener_anchored = True
                taken = max(0, old_hp-fish.hp)
                hydra.hydra_blood_drunk += taken
                if fish.hp <= 0:
                    if hydra.is_companion:
                        # Пойманная ручной гидрой рыба остаётся добычей у её
                        # тела: игрок может забрать улов клавишей E.
                        key = (int(hydra.x), int(hydra.y))
                        self.surface_dropped_items.setdefault(key, []).append(fish.type)
                        self.notification = f'Ручная пиявка-гидра поймала {fish.type.name}!'
                        self.notification_timer = 90
                        hydra.companion_state = f'поймала: {fish.type.name}'
                    self.remove_current_animal(fish)
                    for arm in hydra.hydra_tentacles:
                        if arm.get('target') is fish:
                            arm['attached'] = False
                            arm['target'] = None
                            arm['blood_carry'] = 0.0
            # Высосанная кровь понемногу восстанавливает тело хищника.
            if hydra.hydra_blood_drunk >= 5:
                healing = int(hydra.hydra_blood_drunk // 5)
                hydra.hydra_blood_drunk -= healing*5
                hydra.hp = min(hydra.max_hp, hydra.hp+healing)

    Game.update = update_with_hydras

    def find_targeted_arm(game, x, y, max_distance, from_player=False):
        if game.in_cave:
            return None
        now = pygame.time.get_ticks()
        best = None
        max_dist_sq = max_distance**2
        for hydra in game.get_nearby_animals(VIEW_RADIUS+5):
            if hydra.hp <= 0 or not getattr(hydra.type, 'is_leech_hydra', False):
                continue
            if not hasattr(hydra, 'hydra_tentacles'):
                continue
            if not from_player and math.hypot(x-hydra.x, y-hydra.y) < .62:
                continue  # прицел точно на тело — можно ранить тело (70 HP)
            for index, arm in enumerate(hydra.hydra_tentacles):
                if arm['hp'] <= 0:
                    continue
                tip_x, tip_y = _hydra_tentacle_tip(hydra, arm, now,
                    (game.player_x,game.player_y))
                dist_sq = _hydra_segment_distance_sq(x, y, hydra.x, hydra.y,
                                                    tip_x, tip_y)
                if dist_sq <= max_dist_sq and (best is None or dist_sq < best[0]):
                    best = (dist_sq, hydra, index)
        return best

    def hurt_tentacle(game, hydra, index, damage):
        arm = hydra.hydra_tentacles[index]
        arm['hp'] = max(0, arm['hp']-max(1, int(damage)))
        if arm['hp'] == 0:
            arm['attached'] = False
            arm['target'] = None
            arm['blood_carry'] = 0.0
            arm['regrow_at'] = pygame.time.get_ticks()+HYDRA_REGEN_MS
            game.notification = 'Щупальце пиявки-гидры оторвано! Отрастёт через 26 секунд.'
        else:
            game.notification = f'Щупальце пиявки-гидры: {arm["hp"]}/9 HP'
        game.notification_timer = 80

    # Тестировать, повреждать и отрывать щупальца можно независимо от тела.
    Game._hurt_hydra_tentacle = hurt_tentacle
    old_attack = Game.attack

    def attack_hydra_arm(self):
        if self.state == 'playing' and not self.in_cave:
            hit = find_targeted_arm(self, self.player_x, self.player_y,
                                    self.player_attack_range, True)
            if hit is not None:
                _, hydra, index = hit
                hurt_tentacle(self, hydra, index, self.player_attack_damage)
                return
        return old_attack(self)

    Game.attack = attack_hydra_arm
    old_shoot = Game.shoot

    def shoot_hydra_arm(self, target_x, target_y):
        if (self.state == 'playing' and not self.in_cave
                and self.equipped_weapon is not None):
            weapon = self.equipped_weapon
            if math.hypot(target_x-self.player_x,
                          target_y-self.player_y) <= weapon.weapon_range:
                hit = find_targeted_arm(self, target_x, target_y, .34)
                if hit is not None:
                    _, hydra, index = hit
                    hurt_tentacle(self, hydra, index, weapon.damage)
                    return
        return old_shoot(self, target_x, target_y)

    Game.shoot = shoot_hydra_arm
    old_pick_up = Game.pick_up

    def prevent_live_hydra_pickup(self):
        if not self.in_cave:
            px, py = int(self.player_x), int(self.player_y)
            # Улов ручной гидры лежит рядом с ней: сначала забираем рыбу.
            if self.surface_dropped_items.get((px, py)):
                return old_pick_up(self)
            for hydra in self.get_nearby_animals(1.6):
                if (getattr(hydra.type, 'is_leech_hydra', False)
                        and int(hydra.x) == px and int(hydra.y) == py
                        and hydra.hp > 0):
                    if hydra.is_companion or hydra.hp <= 20:
                        if self.add_to_inventory(hydra.type,
                                                 wild_catch=not hydra.is_companion):
                            self.remove_current_animal(hydra)
                            self.notification = ('Ручная гидра возвращена в инвентарь.'
                                                 if hydra.is_companion else
                                                 'Ослабленная гидра поймана! R — инвентарь, H — приручить.')
                        else:
                            self.notification = 'В инвентаре нет места для гидры!'
                    else:
                        self.notification = 'Гидра сопротивляется! Ослабьте тело до 20 HP.'
                    self.notification_timer = 70
                    return
        return old_pick_up(self)

    Game.pick_up = prevent_live_hydra_pickup
    old_scan_target = Game._scan_target_at_cursor

    def scan_hydra_net(self):
        if not self.in_cave:
            hit = find_targeted_arm(self, self.mouse_world_x,
                                    self.mouse_world_y, .42)
            if hit is not None:
                _, hydra, index = hit
                return {'kind':'animal', 'entity': hydra.type,
                        'animal': hydra, 'x': hydra.x, 'y': hydra.y,
                        'hydra_arm_index': index}
        return old_scan_target(self)

    Game._scan_target_at_cursor = scan_hydra_net
    old_scanner_details = Game._scanner_details

    def scanner_details_with_hydra(self, target):
        if target['entity'] is game_ns.get('leech_hydra_type'):
            hydra = target.get('animal')
            count = len(getattr(hydra, 'hydra_tentacles', ()))
            intact = sum(arm['hp'] > 0 for arm in getattr(hydra, 'hydra_tentacles', ()))
            return [
                'Пиявка-гидра — редкий засадный хищник мелководья.',
                'Большая часть тела скрыта в грунте; длина тела до 1,2 м.',
                f'Тело: 70 HP. Щупалец-присосок: {count or "10–15"}; целых сейчас: {intact}.',
                'Каждое щупальце длиной до 4 м имеет 9 HP.',
                'Повреждённые щупальца можно отсечь ножом или оружием.',
                'Щупальца самостоятельно отрастают через 26 секунд.',
                'Урон: 1 HP × секунды × количество присосавшихся щупалец.',
                'Высасывает кровь из любых рыб, восстанавливая здоровье.',
                'Ручная гидра не кусает хозяина: щупальцами ловит рыбу и оставляет улов.',
                'Для приручения ослабьте тело до 20 HP, E — поймать, затем H в инвентаре.',
                'Уйдите из ловчей сети, чтобы присоски дикой гидры отцепились.',
                'Съедобна, хотя по вкусу напоминает резину.',
            ]
        return old_scanner_details(self, target)

    Game._scanner_details = scanner_details_with_hydra
    old_render = Game.render

    def render_with_hydras(self):
        old_render(self)
        if self.state != 'playing' or self.in_cave:
            return
        display = pygame.display.get_surface()
        zoom = self.camera_zoom
        now = pygame.time.get_ticks()

        def on_screen(x, y):
            return (round(SCREEN_SIZE*.5+(x-self.player_x)*CELL_SIZE*zoom),
                    round(SCREEN_SIZE*.5+(y-self.player_y)*CELL_SIZE*zoom))

        for hydra in self.get_nearby_animals(VIEW_RADIUS+HYDRA_REACH_METERS+1):
            if hydra.hp <= 0 or not getattr(hydra.type, 'is_leech_hydra', False):
                continue
            if not hasattr(hydra, 'hydra_tentacles'):
                continue
            origin = on_screen(hydra.x, hydra.y)
            if not (-170 < origin[0] < SCREEN_SIZE+170
                    and -170 < origin[1] < SCREEN_SIZE+170):
                continue
            for index, arm in enumerate(hydra.hydra_tentacles):
                if arm['hp'] <= 0:
                    continue
                tip_x, tip_y = _hydra_tentacle_tip(hydra, arm, now,
                                                  (self.player_x,self.player_y))
                p0, p3 = origin, on_screen(tip_x, tip_y)
                sway = math.sin(now*.002+arm['phase'])*.18*CELL_SIZE*zoom
                dx, dy = p3[0]-p0[0], p3[1]-p0[1]
                length = max(1.0, math.hypot(dx, dy))
                nx, ny = -dy/length, dx/length
                control = [
                    (round(p0[0]+dx*t+nx*sway*math.sin(math.pi*t)),
                     round(p0[1]+dy*t+ny*sway*math.sin(math.pi*t)))
                    for t in (0.0, .2, .4, .6, .8, 1.0)]
                bright = arm['attached']
                pygame.draw.lines(display, (13, 35, 28), False,
                                  control, max(3, round(6*zoom)))
                pygame.draw.lines(display, (136, 185, 91) if bright else (78, 122, 92),
                                  False, control, max(1, round(3*zoom)))
                cup_radius = max(3, round((7 if bright else 5)*zoom))
                pygame.draw.circle(display, (25, 36, 32), p3, cup_radius+2)
                pygame.draw.circle(display, (176, 88, 97) if bright else (132, 162, 111),
                                   p3, cup_radius, max(2, round(2*zoom)))
                if bright:
                    pygame.draw.circle(display, (247, 121, 105), p3, cup_radius+3, 1)
            # Само тело почти не видно: оно остаётся закамуфлированным под грунтом.
            pygame.draw.circle(display, (44, 61, 44), origin,
                               max(5, round(9*zoom)), max(1, round(2*zoom)))
        pygame.display.flip()

    Game.render = render_with_hydras


install_leech_hydra(globals())
# <<< LEECH_HYDRA_MOD_V1 <<<

# >>> LAND_VEHICLES_AND_RESOURCE_COMPLETENESS_V1 >>>
# Дополнение к МегоСубнатике2D: наземная техника + ресурсы.

def generate_land_vehicle_sprite(kind):
    """Детализированный вид сверху, нос справа; только фигуры pygame."""
    big = kind == 'apc'
    w, h = (192, 112) if big else (136, 86)
    s = pygame.Surface((w, h), pygame.SRCALPHA)
    cx, cy = w // 2, h // 2
    d = pygame.draw
    dark = (35, 39, 34) if big else (39, 45, 45)
    main = (107, 119, 87) if big else (189, 137, 63)
    light = (156, 166, 119) if big else (231, 183, 97)
    # Колёса или шесть бронекатков вдоль двух бортов.
    wheel_xs = (-49, -24, 1, 26, 51, 67) if big else (-36, 32)
    for side in (-1, 1):
        for x in wheel_xs:
            half = 10 if big else 14
            y = cy + side * (34 if big else 26)
            d.rect(s, (16, 19, 21), (cx + x - half // 2, y - 9, half, 18), border_radius=4)
            d.line(s, (71, 78, 76), (cx + x - half // 3, y - 5), (cx + x + half // 3, y - 5), 2)
    body = pygame.Rect(16, 26 if big else 19, w - 32, h - (52 if big else 38))
    d.rect(s, dark, body.inflate(4, 4), border_radius=13 if big else 10)
    d.rect(s, main, body, border_radius=10 if big else 8)
    d.rect(s, light, body.inflate(-10, -12), 2, border_radius=8)
    if big:
        for x in (39, 68, 129):
            d.line(s, (64, 79, 63), (x, 28), (x, 83), 3)
        # Командирская башня, ствол пушки и короткий ствол пулемёта.
        d.circle(s, (45, 55, 45), (102, cy), 29)
        d.circle(s, (122, 141, 105), (102, cy), 25)
        d.polygon(s, (87, 103, 77), [(96, 38), (117, 44), (122, 55), (117, 67), (96, 72), (84, 56)])
        d.rect(s, (32, 39, 38), (111, cy - 6, 75, 11), border_radius=3)
        d.rect(s, (145, 161, 145), (131, cy - 4, 45, 6), border_radius=2)
        d.rect(s, (29, 35, 36), (179, cy - 7, 11, 13), border_radius=2)
        d.rect(s, (38, 39, 42), (103, cy - 23, 63, 5), border_radius=2)
        d.circle(s, (24, 32, 36), (90, cy), 9)
        d.circle(s, (132, 172, 177), (90, cy), 5)
        for yy in (-20, 20):
            d.rect(s, (194, 205, 162), (36, cy + yy - 3, 18, 6), border_radius=2)
    else:
        # Капот, лобовое стекло, багажник и запасное колесо.
        d.polygon(s, (106, 81, 47), [(77, 25), (115, 30), (122, 43), (115, 55), (77, 60)])
        d.line(s, (249, 211, 119), (87, 32), (108, 35), 2)
        d.rect(s, (28, 72, 82), (54, 24, 25, 38), border_radius=5)
        d.rect(s, (93, 163, 164), (59, 28, 15, 29), border_radius=3)
        d.line(s, (46, 62, 61), (65, 28), (65, 56), 2)
        d.circle(s, (22, 25, 28), (24, cy), 11)
        d.circle(s, (115, 118, 112), (24, cy), 7, 2)
        for yy in (29, 55):
            d.circle(s, (250, 238, 173), (118, yy), 4)
    # Джип вдвое меньше по каждой оси (68x43 вместо 136x86).
    if not big:
        return pygame.transform.smoothscale(s, (w // 2, h // 2)).convert_alpha()
    return s.convert_alpha()


def install_land_vehicles_and_resources(game_ns):
    if game_ns.get('_LAND_VEHICLES_RESOURCES_INSTALLED'):
        return
    game_ns['_LAND_VEHICLES_RESOURCES_INSTALLED'] = True
    old_init_assets = game_ns['init_assets']

    def with_land_resources_and_vehicles():
        old_init_assets()
        # Часть исходных объектов определена локально в init_assets().
        cave_sulfur_type = START_ITEM_REGISTRY['Пещерная сера']
        metal_salvage_type = START_ITEM_REGISTRY['Металлолом']
        wiring_kit = START_ITEM_REGISTRY['Комплект проводов']
        advanced_wiring_kit = START_ITEM_REGISTRY['Расширенный комплект проводов']
        global light_jeep_type, armored_carrier_type
        # Новый транспорт становится обычными игровыми предметами.
        light_jeep_type = ObjectType('Лёгкий джип', (204, 151, 75), 20, 'rect')
        light_jeep_type.image = generate_land_vehicle_sprite('jeep')
        light_jeep_type.land_vehicle = True
        armored_carrier_type = ObjectType('Бронетранспортёр', (117, 130, 93), 55, 'rect')
        armored_carrier_type.image = generate_land_vehicle_sprite('apc')
        armored_carrier_type.land_vehicle = True
        game_ns['light_jeep_type'] = light_jeep_type
        game_ns['armored_carrier_type'] = armored_carrier_type

        # Стандартный глобальный «Медный провод» был другим объектом, чем
        # медный провод из рецептов. Объединяем записи в один объект.
        canonical_wire = next((r.result for r in recipes if r.result.name == 'Медный провод'), None)
        if canonical_wire is not None:
            global provod_type
            duplicate_wire = provod_type
            for recipe in recipes:
                recipe.ingredients = {canonical_wire if obj is duplicate_wire else obj: amount
                                      for obj, amount in recipe.ingredients.items()}
                if recipe.result is duplicate_wire:
                    recipe.result = canonical_wire
            provod_type = canonical_wire
            game_ns['provod_type'] = canonical_wire

        # Добавляем недостающие природные ресурсы без возвращения удалённых рыб.
        fossil_tooth = ObjectType('Зуб сталкера', (229, 218, 176), 17, 'rect')
        fossil_tooth.image = generate_crystal_sprite(22, (229, 218, 176), seed=760)
        gas_bubble = ObjectType('Газовый пузырь', (157, 210, 119), 18, 'circle')
        gas_bubble.image = generate_lumpy_sprite(23, gas_bubble.color, seed=761, shine=True)
        game_ns['fossil_tooth_type'] = fossil_tooth
        game_ns['gas_bubble_type'] = gas_bubble
        additions = {
            'Горы': [(coal_type, 1.9), (sulfur_type, 1.0), (uraninite_type, 0.7), (oil_type, 0.4)],
            'Леса': [(coal_type, 1.1), (clay_type, 0.9)],
            'Острова': [(coal_type, 0.75), (uraninite_type, 0.35)],
            'Побережье': [(sulfur_type, 1.1), (oil_type, 1.3), (coal_type, 0.65)],
            'Мелководье': [(cave_sulfur_type, 0.5), (metal_salvage_type, 0.9), (fossil_tooth, 0.28)],
            'Глубоководье': [(uraninite_type, 0.8), (oil_type, 1.1), (gas_bubble, 0.65)],
            'Центральное море': [(oil_type, 1.0), (gas_bubble, 0.85)],
            'Био-люминесцентная зона': [(uraninite_type, 0.35), (cave_sulfur_type, 0.6)],
            'Кельповый лес': [(fossil_tooth, 0.4), (gas_bubble, 0.25)],
            'Коралловый риф': [(cave_sulfur_type, 0.25)],
        }
        # Исключаем отсеки Авроры и непроходимые вспомогательные биомы.
        for b in biomes:
            if not b.passable or getattr(b, 'aurora_style', False):
                continue
            present = {obj.name for obj, density in b.objects}
            for obj, density in additions.get(b.name, ()):
                if obj.name not in present:
                    b.objects.append((obj, density))
                    present.add(obj.name)

        # Все объявленные ранее материалы теперь можно изготовить из сырья.
        # Дополнительные рецепты транспорта используют уже существующие детали.
        new_recipes = [
            Recipe('Сталь', {iron_type: 3, coal_type: 2}, steel_type, 1,
                   'Изготовитель', 'Основные материалы'),
            Recipe('Порох x2', {sulfur_type: 2, coal_type: 1}, gunpowder_type, 2,
                   'Изготовитель', 'Химия'),
            Recipe('Топливо x2', {oil_type: 2, sulfur_type: 1}, fuel_type, 2,
                   'Изготовитель', 'Химия'),
            Recipe('Бетон x2', {limestone_type: 2, sand_type: 2, clay_type: 1}, concrete_type, 2,
                   'Изготовитель', 'Основные материалы'),
            Recipe('Лёгкий джип', {steel_type: 2, titan_type: 4, copper_type: 2,
                                  glass_type: 1, fuel_type: 1, wiring_kit: 1},
                   light_jeep_type, 1, 'Переносной сборщик транспорта', 'Наземная техника'),
            Recipe('Бронетранспортёр', {steel_type: 7, titan_type: 8, lead_type: 4,
                                      diamond_type: 2, advanced_wiring_kit: 2,
                                      fuel_type: 3, gunpowder_type: 3},
                   armored_carrier_type, 1, 'Переносной сборщик транспорта', 'Наземная техника'),
        ]
        for recipe in new_recipes:
            if not any(existing.name == recipe.name for existing in recipes):
                recipes.append(recipe)
        # Регистры начальных предметов создаются после исходных рецептов,
        # поэтому новым объектам нужна собственная регистрация.
        START_ITEM_REGISTRY.update({
            light_jeep_type.name: light_jeep_type,
            armored_carrier_type.name: armored_carrier_type,
            'light_jeep_type': light_jeep_type,
            'armored_carrier_type': armored_carrier_type,
            fossil_tooth.name: fossil_tooth,
            gas_bubble.name: gas_bubble,
            canonical_wire.name: canonical_wire,
        } if canonical_wire is not None else {
            light_jeep_type.name: light_jeep_type,
            armored_carrier_type.name: armored_carrier_type,
            fossil_tooth.name: fossil_tooth,
            gas_bubble.name: gas_bubble,
        })

    game_ns['init_assets'] = with_land_resources_and_vehicles

    # Гарантируем, что новые ресурсы действительно появятся даже в маленьких
    # или перекрытых полигонах; не размещаем в стенах, строениях и на воде для
    # наземного сырья, если конкретный биом задан как суша.
    old_world_gen = World._generate_objects

    def generate_objects_with_missing_resources(self):
        old_world_gen(self)
        by_name = {name: [] for name in ('Уголь', 'Сера', 'Пещерная сера',
                  'Нефть', 'Уранинит', 'Зуб сталкера', 'Газовый пузырь')}
        for b in self.biomes:
            if getattr(b, 'aurora_style', False) or not b.passable:
                continue
            for resource, density in b.objects:
                if resource.name in by_name:
                    by_name[resource.name].append((b, resource))
        actual = {name: 0 for name in by_name}
        for obj in self.object_grid.values():
            if obj.name in actual:
                actual[obj.name] += 1
        rng = random.Random(202610301)
        for name, destinations in by_name.items():
            if not destinations:
                continue
            missing = max(0, 10 - actual[name])
            attempts = 0
            while missing and attempts < 2800:
                attempts += 1
                biome, item = destinations[attempts % len(destinations)]
                x, y = biome.random_point(rng)
                key = (int(x), int(y))
                if (self.get_biome_at(*key) is biome and key not in self.object_grid):
                    self.object_grid[key] = item
                    missing -= 1

    World._generate_objects = generate_objects_with_missing_resources

    # Особо пещерный материал дополнительно находится именно в пещерах.
    old_caves = Game._init_caves

    def init_caves_with_sulfur(self):
        old_caves(self)
        cave_sulfur_type = START_ITEM_REGISTRY['Пещерная сера']
        rng = random.Random(202610302)
        for tunnel in self.cave_tunnels:
            if getattr(tunnel, 'cave_kind', '') != 'hub':
                continue
            parent = getattr(tunnel, 'parent_surface_biome', None)
            if parent is None or parent.name not in ('Мелководье', 'Горы', 'Био-люминесцентная зона'):
                continue
            for i in range(3):
                for attempt in range(150):
                    x, y = tunnel.random_point(rng)
                    key = (int(x), int(y))
                    if key not in self.cave_object_grid and self.get_cave_tunnel_at(*key) is not None:
                        self.cave_object_grid[key] = cave_sulfur_type
                        # Численность фиксируется позже _capture_ecosystem_targets().
                        break

    Game._init_caves = init_caves_with_sulfur

    old_game_init = Game.__init__

    def init_with_land_vehicles(self):
        old_game_init(self)
        self.vehicle_max_hp.update({light_jeep_type: 15, armored_carrier_type: 200})
        self.vehicle_hp.update({light_jeep_type: 15, armored_carrier_type: 200})
        self.armored_mg_cooldown = 0
        self.armored_heavy_cooldown = 0

    Game.__init__ = init_with_land_vehicles

    def handle_vehicle_action_with_land(self):
        if self.state not in ('playing', 'inventory'):
            return
        if self.active_vehicle is not None:
            vehicle = self.active_vehicle
            self.active_vehicle = None
            self.state = 'playing'
            self.notification = f'Вы покинули: {vehicle.name}'
            self.notification_timer = 80
            return
        on_land = self._is_player_on_land()
        # Ищем ближайшие только совместимые с типом местности обломки.
        near_wrecks = [w for w in self.vehicle_wrecks if w['in_cave'] == self.in_cave
                       and bool(getattr(w['type'], 'land_vehicle', False)) == on_land
                       and (w['x'] - self.player_x) ** 2 + (w['y'] - self.player_y) ** 2 <= 1.7 ** 2]
        if near_wrecks:
            wreck = min(near_wrecks, key=lambda w: (w['x'] - self.player_x) ** 2 + (w['y'] - self.player_y) ** 2)
            kind = wreck['type']
            kits = 2 if kind in (cyclops_type, armored_carrier_type) else 1
            if self.inventory_counts.get(repair_kit, 0) < kits:
                self.notification = f'Для ремонта {kind.name} требуется {kits} ремкомплект(а)'
                self.notification_timer = 95
                return
            if not any(item is None for item in self.inventory):
                self.notification = 'Нужно место в инвентаре для восстановленного транспорта'
                self.notification_timer = 95
                return
            if not self._remove_inventory_items_of_type(repair_kit, kits):
                return
            if not self.add_to_inventory(kind):
                for _ in range(kits):
                    self.add_to_inventory(repair_kit)
                return
            self.vehicle_hp[kind] = self.vehicle_max_hp[kind]
            self.vehicle_wrecks.remove(wreck)
            self.active_vehicle = kind
            self.state = 'playing'
            self.notification = f'{kind.name} восстановлен и готов к работе'
            self.notification_timer = 95
            return
        selected = None
        if 0 <= self.active_slot_index < len(self.inventory):
            item = self.inventory[self.active_slot_index]
            if item in self.vehicle_max_hp:
                selected = item
        # При явном выборе несовместимого транспорта не подменяем его другим.
        if selected is not None and bool(getattr(selected, 'land_vehicle', False)) != on_land:
            self.notification = ('Наземную технику можно использовать только на суше' if not on_land
                                 else 'Подлодки можно использовать только в воде')
            self.notification_timer = 100
            return
        if selected is None:
            selected = next((item for item in self.inventory
                             if item in self.vehicle_max_hp
                             and bool(getattr(item, 'land_vehicle', False)) == on_land), None)
        if selected is None:
            self.notification = ('В инвентаре нет наземного транспорта' if on_land
                                 else 'В инвентаре нет подлодки')
            self.notification_timer = 80
            return
        if self.vehicle_hp.get(selected, 0) <= 0:
            self.notification = f'{selected.name} уничтожен: вернитесь к месту крушения'
            self.notification_timer = 80
            return
        self.active_vehicle = selected
        self.state = 'playing'
        if selected is armored_carrier_type:
            self.notification = 'БТР: WASD — движение, ЛКМ — тяжёлая пушка (3 с), ПКМ — пулемёт (0,2 с), F — выйти'
        elif selected is light_jeep_type:
            self.notification = 'Джип: WASD — движение, ЛКМ — экипированное оружие, F — выйти'
        else:
            self.notification = f'{selected.name}: WASD — движение, ЛКМ — торпеда, F — выйти'
        self.notification_timer = 180

    Game.handle_vehicle_action = handle_vehicle_action_with_land

    def _land_vehicle_shot(self, target_x, target_y, kind):
        if self.active_vehicle is not armored_carrier_type or self.state != 'playing' or self.in_cave:
            return
        if kind == 'heavy':
            if self.armored_heavy_cooldown > 0:
                return
            damage, speed, cooldown, ttl = (30, 0.86, 180, 40)
            self.armored_heavy_cooldown = cooldown
        else:
            if self.armored_mg_cooldown > 0:
                return
            damage, speed, cooldown, ttl = (1, 1.10, 12, 27)
            self.armored_mg_cooldown = cooldown
        dx, dy = (target_x - self.player_x, target_y - self.player_y)
        length = math.hypot(dx, dy)
        if length < 0.001:
            angle = math.radians(self.player_angle)
            dx, dy = math.cos(angle), math.sin(angle)
        else:
            dx, dy = dx / length, dy / length
        self.vehicle_torpedoes.append({'x': self.player_x + dx * 0.8,
                'y': self.player_y + dy * 0.8, 'vx': dx * speed,
                'vy': dy * speed, 'damage': damage, 'ttl': ttl,
                'in_cave': False, 'weapon_kind': kind})

    Game._land_vehicle_shot = _land_vehicle_shot
    old_torpedo = Game.fire_vehicle_torpedo

    def fire_with_land_vehicles(self, x, y):
        if self.active_vehicle is light_jeep_type:
            if self.equipped_weapon is not None and self.equipped_weapon.is_weapon:
                self.shoot(x, y)
            else:
                self.notification = 'Для стрельбы из джипа экипируйте оружие клавишей Q'
                self.notification_timer = 85
            return
        if self.active_vehicle is armored_carrier_type:
            self._land_vehicle_shot(x, y, 'heavy')
            return
        return old_torpedo(self, x, y)

    Game.fire_vehicle_torpedo = fire_with_land_vehicles
    old_update = Game.update

    def update_with_land_weapons(self):
        if self.state == 'playing' and not self.settings_open:
            if self.armored_mg_cooldown > 0:
                self.armored_mg_cooldown -= 1
            if self.armored_heavy_cooldown > 0:
                self.armored_heavy_cooldown -= 1
            if self.active_vehicle is armored_carrier_type and pygame.mouse.get_pressed(num_buttons=3)[2]:
                mouse_pos = pygame.mouse.get_pos()
                x, y = self.screen_to_world(*mouse_pos)
                self._land_vehicle_shot(x, y, 'machine_gun')
        return old_update(self)

    Game.update = update_with_land_weapons

    old_draw_bullets = Game.draw_vehicle_torpedoes

    def draw_land_vehicle_bullets(self, camera_x, camera_y):
        # Оригинальные торпеды рисуются через прежний метод; новые рисуем поверх.
        old_draw_bullets(self, camera_x, camera_y)
        for shot in self.vehicle_torpedoes:
            if shot.get('weapon_kind') not in ('heavy', 'machine_gun') or shot['in_cave'] != self.in_cave:
                continue
            sx = round((shot['x'] - camera_x) * CELL_SIZE + CELL_SIZE / 2)
            sy = round((shot['y'] - camera_y) * CELL_SIZE + CELL_SIZE / 2)
            if shot['weapon_kind'] == 'heavy':
                pygame.draw.circle(self.screen, (244, 145, 57), (sx, sy), 6)
                pygame.draw.circle(self.screen, (255, 222, 130), (sx - 1, sy - 1), 3)
            else:
                pygame.draw.circle(self.screen, (255, 240, 152), (sx, sy), 3)

    Game.draw_vehicle_torpedoes = draw_land_vehicle_bullets

install_land_vehicles_and_resources(globals())
# <<< LAND_VEHICLES_AND_RESOURCE_COMPLETENESS_V1 <<<

# >>> EQUIPPED_WEAPON_PROJECTILES_V1 >>>
# Боеприпасы для всего экипируемого оружия и реальные летящие пули.
# Одна коробка/аккумулятор/канистра занимает одну ячейку и даёт 20 выстрелов.
WEAPON_AMMO_CAPACITY = 20


def install_equipped_weapon_projectiles(game_ns):
    if game_ns.get('_EQUIPPED_WEAPON_PROJECTILES_INSTALLED'):
        return
    game_ns['_EQUIPPED_WEAPON_PROJECTILES_INSTALLED'] = True
    old_init_assets = game_ns['init_assets']

    def init_armed_assets():
        old_init_assets()
        global regular_ammo_type, energy_ammo_type, flame_ammo_type
        regular_ammo_type = ObjectType('Коробка патронов (20)', (198, 174, 95), 23, 'rect')
        energy_ammo_type = ObjectType('Энергозаряды (20)', (65, 196, 249), 23, 'rect')
        flame_ammo_type = ObjectType('Огнесмесь (20)', (235, 126, 47), 23, 'rect')
        for item in (regular_ammo_type, energy_ammo_type, flame_ammo_type):
            item.image = generate_tech_item_sprite(item.name, item.size, item.color)
            item.ammo_capacity = WEAPON_AMMO_CAPACITY
            START_ITEM_REGISTRY[item.name] = item
            game_ns[item.name] = item
        game_ns['regular_ammo_type'] = regular_ammo_type
        game_ns['energy_ammo_type'] = energy_ammo_type
        game_ns['flame_ammo_type'] = flame_ammo_type

        # Материалы берём из актуального регистра после всех предыдущих модов.
        def resource(name):
            return START_ITEM_REGISTRY[name]

        recipes_new = [
            Recipe('Коробка патронов (20)',
                {iron_type: 1, copper_type: 1, gunpowder_type: 1}, regular_ammo_type,
                1, 'Изготовитель', 'Боеприпасы'),
            Recipe('Энергозаряды (20)',
                {resource('Батарея'): 1, quartz_type: 1, resource('Медный провод'): 1},
                energy_ammo_type, 1, 'Изготовитель', 'Боеприпасы'),
            Recipe('Огнесмесь (20)',
                {fuel_type: 1, sulfur_type: 1}, flame_ammo_type,
                1, 'Изготовитель', 'Боеприпасы'),
            Recipe('Лазерный пистолет',
                {titan_type: 2, resource('Батарея'): 1,
                 resource('Медный провод'): 1, glass_type: 1},
                laser_pistol, 1, 'Изготовитель', 'Оружие'),
            Recipe('Подводное ружьё',
                {steel_type: 2, resource('Медный провод'): 1,
                 glass_type: 1, resource('Силиконовая резина'): 1},
                underwater_rifle, 1, 'Изготовитель', 'Оружие'),
            Recipe('Плазменная пушка',
                {magnetite_type: 2, resource('Батарея'): 2,
                 resource('Микросхема'): 1, resource('Полианилин'): 1},
                plasma_cannon, 1, 'Изготовитель', 'Оружие'),
            Recipe('Дальняя пушка',
                {steel_type: 3, glass_type: 2,
                 resource('Расширенный комплект проводов'): 1},
                lang_cannon, 1, 'Изготовитель', 'Оружие'),
            Recipe('Тяжёлая пушка',
                {steel_type: 5, lead_type: 3, diamond_type: 1,
                 resource('Расширенный комплект проводов'): 1},
                schweer_cannon, 1, 'Изготовитель', 'Оружие'),
            Recipe('Алмазный резак',
                {diamond_type: 3, steel_type: 2, resource('Батарея'): 1},
                diamond_cutter, 1, 'Изготовитель', 'Оружие'),
            Recipe('Огнемёт',
                {steel_type: 2, fuel_type: 2, resource('Медный провод'): 1},
                flamethrower, 1, 'Изготовитель', 'Оружие'),
            Recipe('Золотой пистолет',
                {gold_type: 3, steel_type: 1, resource('Батарея'): 1,
                 resource('Микросхема'): 1},
                golden_pistol, 1, 'Изготовитель', 'Оружие'),
        ]
        names = {recipe.name for recipe in recipes}
        for recipe in recipes_new:
            if recipe.name not in names:
                recipes.append(recipe)
                names.add(recipe.name)
        for item in (laser_pistol, underwater_rifle, plasma_cannon,
                     lang_cannon, schweer_cannon, diamond_cutter,
                     flamethrower, golden_pistol):
            START_ITEM_REGISTRY[item.name] = item

    game_ns['init_assets'] = init_armed_assets

    old_game_init = Game.__init__

    def init_armed_game(self):
        old_game_init(self)
        self.player_bullets = []
        self.weapon_shot_cooldown = 0
        # Заряженные патроны остаются в магазине, даже когда коробка уже потрачена.
        self.loaded_weapon_ammo = {}

    Game.__init__ = init_armed_game

    def _weapon_ammo_type(self, weapon):
        if weapon is None or not getattr(weapon, 'is_weapon', False):
            return None
        t = getattr(weapon, 'weapon_type', '')
        if t == 'огонь':
            return flame_ammo_type
        if t in ('лазер', 'плазма'):
            return energy_ammo_type
        return regular_ammo_type

    Game._weapon_ammo_type = _weapon_ammo_type

    def _ammo_remaining(self, ammo_type):
        if ammo_type is None:
            return 0
        return (self.loaded_weapon_ammo.get(ammo_type, 0) +
                self.inventory_counts.get(ammo_type, 0) * WEAPON_AMMO_CAPACITY)

    Game._ammo_remaining = _ammo_remaining

    def fire_equipped_weapon_bullet(self, target_x, target_y):
        if self.state != 'playing' or self.settings_open:
            return
        if self.active_vehicle is not None and self.active_vehicle is not light_jeep_type:
            return
        weapon = self.equipped_weapon
        if weapon is None or not getattr(weapon, 'is_weapon', False):
            self.notification = 'Сначала экипируйте оружие: выберите его в R, затем Q.'
            self.notification_timer = 75
            return
        if self.weapon_shot_cooldown > 0 or len(self.player_bullets) >= 160:
            return
        dx = target_x - self.player_x
        dy = target_y - self.player_y
        dist = math.hypot(dx, dy)
        if dist < 0.001:
            a = math.radians(self.player_angle)
            dx, dy = math.cos(a), math.sin(a)
        else:
            dx, dy = dx / dist, dy / dist
        ammo_type = self._weapon_ammo_type(weapon)
        available = self.loaded_weapon_ammo.get(ammo_type, 0)
        if available == 0:
            if self.inventory_counts.get(ammo_type, 0) <= 0:
                self.notification = f'Нет боеприпасов: {ammo_type.name}. Изготовьте их в крафте (I).'
                self.notification_timer = 85
                return
            if not self._remove_inventory_items_of_type(ammo_type, 1):
                return
            available = WEAPON_AMMO_CAPACITY
        self.loaded_weapon_ammo[ammo_type] = available - 1
        category = getattr(weapon, 'weapon_type', '')
        params = {
            'ружьё': (0.85, (250, 217, 128), 2.7, 10),
            'лазер': (1.1, (95, 238, 252), 3.5, 10),
            'плазма': (0.53, (232, 86, 250), 5.0, 22),
            'огонь': (0.43, (255, 124, 36), 4.5, 5),
        }
        speed, color, radius, cooldown = params.get(category, params['ружьё'])
        if weapon is schweer_cannon:
            cooldown = 30
        elif weapon is lang_cannon:
            cooldown = 7
        self.weapon_shot_cooldown = cooldown
        self.player_bullets.append({
            'x': self.player_x + dx * 0.38,
            'y': self.player_y + dy * 0.38,
            'vx': dx * speed, 'vy': dy * speed,
            'distance': max(0.4, float(weapon.weapon_range)),
            'damage': int(weapon.damage), 'color': color,
            'radius': radius, 'kind': category,
            'in_cave': bool(self.in_cave), 'weapon_name': weapon.name,
        })

    Game.shoot = fire_equipped_weapon_bullet

    # FAST_BULLET_COLLISION_V2: единое столкновение траектории с целями
    # вместо ~6-25 полных переборов фауны, гидр и растений за кадр.
    def _bullet_circle_t(sx, sy, dx, dy, cx, cy, radius):
        """Первое попадание отрезка P(t)=P0+t*(dx,dy) в окружность, t∈[0,1]."""
        rx = cx - sx
        ry = cy - sy
        if abs(rx) > abs(dx) + radius or abs(ry) > abs(dy) + radius:
            return None
        r2 = radius * radius
        dist0 = rx * rx + ry * ry
        if dist0 <= r2:
            return 0.0
        length2 = dx * dx + dy * dy
        if length2 < 1e-12:
            return None
        t_center = (rx * dx + ry * dy) / length2
        if t_center < 0.0 or t_center > 1.0 + radius / math.sqrt(length2):
            return None
        perp2 = dist0 - t_center * t_center * length2
        if perp2 > r2:
            return None
        t = t_center - math.sqrt(max(0.0, r2 - perp2) / length2)
        return max(0.0, t) if t <= 1.0 else None

    def _bullet_capsule_t(sx, sy, dx, dy, ax, ay, bx, by, radius):
        """Отрезок пули против щупальца (отрезок с радиусом)."""
        ux, uy = bx - ax, by - ay
        length = math.hypot(ux, uy)
        if length < 1e-9:
            return _bullet_circle_t(sx, sy, dx, dy, ax, ay, radius)
        ux /= length
        uy /= length
        # Пересечение двух линейных интервалов: расстояние от оси
        # щупальца <= radius и проекция на его длину от 0 до length.
        rx, ry = sx - ax, sy - ay
        side0 = rx * uy - ry * ux
        side_speed = dx * uy - dy * ux
        along0 = rx * ux + ry * uy
        along_speed = dx * ux + dy * uy
        low, high = 0.0, 1.0
        for value, speed, minimum, maximum in (
            (side0, side_speed, -radius, radius),
            (along0, along_speed, 0.0, length),
        ):
            if abs(speed) < 1e-12:
                if value < minimum or value > maximum:
                    low = 2.0
                    break
                continue
            t0, t1 = (minimum - value) / speed, (maximum - value) / speed
            if t0 > t1:
                t0, t1 = t1, t0
            low = max(low, t0)
            high = min(high, t1)
            if low > high:
                break
        result = low if low <= high else None
        for px, py in ((ax, ay), (bx, by)):
            t = _bullet_circle_t(sx, sy, dx, dy, px, py, radius)
            if t is not None and (result is None or t < result):
                result = t
        return result

    def _bullet_wall_t(game, sx, sy, dx, dy):
        """Пройти клетки траектории один раз (DDA), не пропуская стены."""
        x, y = math.floor(sx), math.floor(sy)
        if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
            return 0.0
        endx, endy = sx + dx, sy + dy
        if game.in_cave:
            # Пещеры имеют не только клетки, но и наклонные границы.
            # Проверяем только геометрию стен, без перебора животных.
            steps = max(1, math.ceil(math.hypot(dx, dy) / 0.16))
            for i in range(1, steps + 1):
                t = i / steps
                xx, yy = sx + t * dx, sy + t * dy
                if (not (0 <= xx < MAP_SIZE and 0 <= yy < MAP_SIZE)
                        or not game.cave_is_passable(xx, yy)):
                    return t
            return None

        walls = game.world.object_grid
        def blocked(ix, iy):
            if not (0 <= ix < MAP_SIZE and 0 <= iy < MAP_SIZE):
                return True
            obj = walls.get((ix, iy))
            if (obj is not None and obj.is_wall) or game.is_start_capsule_wall(ix, iy):
                return True
            biome = game.world.get_biome_at(ix, iy)
            return biome is not None and not biome.passable

        if blocked(x, y):
            return 0.0
        if abs(dx) < 1e-12 and abs(dy) < 1e-12:
            return None
        step_x = 1 if dx > 0 else -1
        step_y = 1 if dy > 0 else -1
        tx = (((x + 1 if dx > 0 else x) - sx) / dx) if dx else float('inf')
        ty = (((y + 1 if dy > 0 else y) - sy) / dy) if dy else float('inf')
        dtx = abs(1.0 / dx) if dx else float('inf')
        dty = abs(1.0 / dy) if dy else float('inf')
        while min(tx, ty) <= 1.0 + 1e-12:
            t = min(tx, ty)
            if abs(tx - ty) < 1e-10:
                # Угол стены: не протискиваться между диагональными блоками.
                if blocked(x + step_x, y) or blocked(x, y + step_y):
                    return max(0.0, t)
                x += step_x
                y += step_y
                tx += dtx
                ty += dty
            elif tx < ty:
                x += step_x
                tx += dtx
            else:
                y += step_y
                ty += dty
            if blocked(x, y):
                return max(0.0, t)
        return None

    def _bullet_frame_buckets(game, size=6):
        """Единый индекс малочисленной наземной фауны и мха на кадр."""
        golems, moss = {}, {}
        for g in game.world.golem_colonies:
            if g.phase == 'active':
                golems.setdefault((math.floor(g.x / size), math.floor(g.y / size)), []).append(g)
        for m in game.world.moss_colonies:
            moss.setdefault((math.floor(m.x / size), math.floor(m.y / size)), []).append(m)
        return golems, moss

    def _bullet_objects_in_path(buckets, sx, sy, ex, ey, radius, size=6):
        x0 = math.floor((min(sx, ex) - radius) / size)
        x1 = math.floor((max(sx, ex) + radius) / size)
        y0 = math.floor((min(sy, ey) - radius) / size)
        y1 = math.floor((max(sy, ey) + radius) / size)
        for cy in range(y0, y1 + 1):
            for cx in range(x0, x1 + 1):
                yield from buckets.get((cx, cy), ())

    def _update_equipped_weapon_bullets(self):
        if not self.player_bullets:
            return
        survivors = []
        # Построить индексы лишь один раз на кадр и только если есть пули.
        if self.in_cave:
            ground_golems = ground_moss = None
        else:
            ground_golems, ground_moss = _bullet_frame_buckets(self)
        hydra_shapes = {}
        current_time = None
        for shot in self.player_bullets:
            if shot['in_cave'] != self.in_cave:
                survivors.append(shot)
                continue
            move = min(shot['distance'], math.hypot(shot['vx'], shot['vy']))
            if move <= 1e-9:
                continue
            sx, sy = shot['x'], shot['y']
            velocity = math.hypot(shot['vx'], shot['vy'])
            dx, dy = shot['vx'] * move / velocity, shot['vy'] * move / velocity
            ex, ey = sx + dx, sy + dy
            best_t = _bullet_wall_t(self, sx, sy, dx, dy)
            best_kind = 'wall' if best_t is not None else None
            best_obj = None
            best_index = None

            # Surface: индекс существ по чанкам; cave: фильтрация локальной
            # области (пещерная фауна не входит в индекс поверхности).
            if self.in_cave:
                nearby = (a for a in self.cave_animals
                          if (min(sx, ex)-1.45 <= a.x <= max(sx, ex)+1.45
                              and min(sy, ey)-1.45 <= a.y <= max(sy, ey)+1.45))
            else:
                mx, my = (sx + ex) * 0.5, (sy + ey) * 0.5
                radius = move * .5 + HYDRA_REACH_METERS + 1.5
                nearby = self.world.get_animals_near(mx, my, radius)

            for animal in nearby:
                if animal.hp <= 0 or getattr(animal, 'is_companion', False):
                    continue
                # Если луч проходит над дальним существом, всё остальное
                # для него можно пропустить без квадратного корня.
                if (animal.x < min(sx, ex)-5.0 or animal.x > max(sx, ex)+5.0
                        or animal.y < min(sy, ey)-5.0 or animal.y > max(sy, ey)+5.0):
                    continue
                if not self.in_cave and getattr(animal.type, 'is_leech_hydra', False) and getattr(animal, 'hydra_tentacles', None):
                    if animal not in hydra_shapes:
                        if current_time is None:
                            current_time = pygame.time.get_ticks()
                        hydra_shapes[animal] = [
                            (i, *_hydra_tentacle_tip(animal, arm, current_time,
                                                       (self.player_x, self.player_y)))
                            for i, arm in enumerate(animal.hydra_tentacles) if arm['hp'] > 0
                        ]
                    for arm_index, tx, ty in hydra_shapes[animal]:
                        t = _bullet_capsule_t(sx, sy, dx, dy, animal.x, animal.y, tx, ty, .18)
                        if t is None or (best_t is not None and t >= best_t):
                            continue
                        # Внутри центра гидры стреляем по телу, не по щупальцу.
                        bx, by = sx + t * dx - animal.x, sy + t * dy - animal.y
                        if bx * bx + by * by < .65 * .65:
                            continue
                        best_t, best_kind, best_obj, best_index = t, 'arm', animal, arm_index

                hit_radius = max(.24, min(1.45, animal.type.size / CELL_SIZE * .28))
                t = _bullet_circle_t(sx, sy, dx, dy, animal.x, animal.y, hit_radius)
                if t is not None and (best_t is None or t < best_t):
                    best_t, best_kind, best_obj, best_index = t, 'animal', animal, None

            if not self.in_cave:
                for g in _bullet_objects_in_path(ground_golems, sx, sy, ex, ey, .3):
                    t = _bullet_circle_t(sx, sy, dx, dy, g.x, g.y, .3)
                    if t is not None and (best_t is None or t < best_t):
                        best_t, best_kind, best_obj, best_index = t, 'golem', g, None
                for m in _bullet_objects_in_path(ground_moss, sx, sy, ex, ey, .28):
                    t = _bullet_circle_t(sx, sy, dx, dy, m.x, m.y, .28)
                    if t is not None and (best_t is None or t < best_t):
                        best_t, best_kind, best_obj, best_index = t, 'moss', m, None

            if best_t is not None:
                # Пуля остаётся на точке контакта и сразу удаляется.
                shot['x'] = sx + dx * best_t
                shot['y'] = sy + dy * best_t
                if best_kind == 'arm':
                    self._hurt_hydra_tentacle(best_obj, best_index, shot['damage'])
                elif best_kind == 'animal':
                    animal = best_obj
                    animal.hp -= shot['damage']
                    self.notification = (f'{shot["weapon_name"]}: {animal.type.name}, '
                                         f'HP {max(0, int(animal.hp))}')
                    self.notification_timer = 40
                    if animal.hp <= 0:
                        self.handle_animal_death(animal)
                elif best_kind == 'golem':
                    self._damage_golem(best_obj, shot['damage'])
                elif best_kind == 'moss':
                    self._hit_mad_moss(best_obj, shot['damage'])
                    ground_golems, ground_moss = _bullet_frame_buckets(self)
                elif best_kind == 'wall':
                    _damage_wall_at_shot_contact(self, shot, sx, sy, dx, dy, best_t)
                continue
            shot['x'], shot['y'] = ex, ey
            shot['distance'] -= move
            if shot['distance'] > 1e-6:
                survivors.append(shot)
        self.player_bullets = survivors

    Game._update_equipped_weapon_bullets = _update_equipped_weapon_bullets

    def _update_fast_vehicle_torpedoes(self):
        """БТР/подлодки: столкновения с локальными целями вместо обхода всей фауны."""
        if self.vehicle_torpedo_cooldown > 0:
            self.vehicle_torpedo_cooldown -= 1
        if not self.vehicle_torpedoes:
            return
        survivors = []
        if self.in_cave:
            ground_golems = ground_moss = None
        else:
            ground_golems, ground_moss = _bullet_frame_buckets(self)
        for shot in self.vehicle_torpedoes:
            if shot['in_cave'] != self.in_cave:
                continue
            shot['ttl'] -= 1
            if shot['ttl'] <= 0:
                continue
            sx, sy = shot['x'], shot['y']
            dx, dy = shot['vx'], shot['vy']
            ex, ey = sx + dx, sy + dy
            best_t = _bullet_wall_t(self, sx, sy, dx, dy)
            best_kind = 'wall' if best_t is not None else None
            best_obj = None
            if self.in_cave:
                nearby = (a for a in self.cave_animals
                          if min(sx, ex) - 8 <= a.x <= max(sx, ex) + 8
                          and min(sy, ey) - 8 <= a.y <= max(sy, ey) + 8)
            else:
                mx, my = (sx + ex) / 2, (sy + ey) / 2
                nearby = self.world.get_animals_near(mx, my, math.hypot(dx, dy) / 2 + 8)
            for animal in nearby:
                if animal.hp <= 0 or getattr(animal, 'is_companion', False):
                    continue
                visual_radius = max(.35, animal.type.size / CELL_SIZE * .32)
                t = _bullet_circle_t(sx, sy, dx, dy, animal.x, animal.y, visual_radius)
                if t is not None and (best_t is None or t < best_t):
                    best_t, best_kind, best_obj = t, 'animal', animal
            if not self.in_cave:
                for g in _bullet_objects_in_path(ground_golems, sx, sy, ex, ey, .9):
                    t = _bullet_circle_t(sx, sy, dx, dy, g.x, g.y, .9)
                    if t is not None and (best_t is None or t < best_t):
                        best_t, best_kind, best_obj = t, 'golem', g
                for m in _bullet_objects_in_path(ground_moss, sx, sy, ex, ey, .55):
                    t = _bullet_circle_t(sx, sy, dx, dy, m.x, m.y, .55)
                    if t is not None and (best_t is None or t < best_t):
                        best_t, best_kind, best_obj = t, 'moss', m
            if best_t is not None:
                if best_kind == 'animal':
                    a = best_obj
                    a.hp -= shot['damage']
                    self.notification = f'Торпеда поразила {a.type.name}: HP {max(0, int(a.hp))}'
                    self.notification_timer = 45
                    if a.hp <= 0:
                        self.handle_animal_death(a)
                elif best_kind == 'golem':
                    self._damage_golem(best_obj, shot['damage'])
                elif best_kind == 'moss':
                    self._hit_mad_moss(best_obj, shot['damage'])
                    # При делении мха индекс должен отразить новые колонии.
                    ground_golems, ground_moss = _bullet_frame_buckets(self)
                elif best_kind == 'wall':
                    _damage_wall_at_shot_contact(self, shot, sx, sy, dx, dy, best_t)
                continue
            shot['x'], shot['y'] = ex, ey
            survivors.append(shot)
        self.vehicle_torpedoes = survivors

    Game._update_vehicle_torpedoes = _update_fast_vehicle_torpedoes
    old_update = Game.update

    def update_with_player_bullets(self):
        if self.state == 'playing' and not self.settings_open:
            if self.weapon_shot_cooldown > 0:
                self.weapon_shot_cooldown -= 1
        result = old_update(self)
        if self.state == 'playing' and not self.settings_open:
            self._update_equipped_weapon_bullets()
        return result

    Game.update = update_with_player_bullets
    old_draw_torpedoes = Game.draw_vehicle_torpedoes

    def draw_equipped_weapon_bullets(self, camera_x, camera_y):
        old_draw_torpedoes(self, camera_x, camera_y)
        for shot in self.player_bullets:
            if shot['in_cave'] != self.in_cave:
                continue
            x = round((shot['x'] - camera_x) * CELL_SIZE + CELL_SIZE / 2)
            y = round((shot['y'] - camera_y) * CELL_SIZE + CELL_SIZE / 2)
            view_size = getattr(self, '_render_canvas_size', SCREEN_SIZE)
            if x < -24 or y < -24 or x > view_size + 24 or y > view_size + 24:
                continue
            length = max(1e-8, math.hypot(shot['vx'], shot['vy']))
            dx, dy = shot['vx']/length, shot['vy']/length
            px = round(x - dx * (shot['radius'] * 3))
            py = round(y - dy * (shot['radius'] * 3))
            pygame.draw.line(self.screen, shot['color'], (px, py), (x, y),
                             max(2, round(shot['radius'])))
            pygame.draw.circle(self.screen, (255, 252, 229), (x, y),
                               max(2, round(shot['radius'] * .65)))

    Game.draw_vehicle_torpedoes = draw_equipped_weapon_bullets
    old_draw_hud = Game.draw_hud

    def draw_ammo_hud(self):
        old_draw_hud(self)
        if (self.equipped_weapon is None or self.state != 'playing' or
                (self.active_vehicle is not None and self.active_vehicle is not light_jeep_type)):
            return
        ammo_type = self._weapon_ammo_type(self.equipped_weapon)
        if ammo_type is None:
            return
        count = self._ammo_remaining(ammo_type)
        label = 'Джип' if self.active_vehicle is light_jeep_type else 'Оружие'
        line = self.small_font.render(f'{label} | {ammo_type.name}: {count} выстр.',
                                      True, (245, 213, 127) if count else (255, 96, 88))
        self.screen.blit(line, (10, 252))

    Game.draw_hud = draw_ammo_hud


install_equipped_weapon_projectiles(globals())
# <<< EQUIPPED_WEAPON_PROJECTILES_V1 <<<


# >>> AURORA_SPECIALIZED_STOCK_V2 >>>
# Складские отделения: каждый отсек имеет собственную номенклатуру.
# В оружейном хранилище разрешено смешение оружия с коробками патронов.
# Точные названия предметов должны совпадать с START_ITEM_REGISTRY.
AURORA_ROOM_STOCK = {
    'Входной коридор Авроры / Airlock': ('Батарея',),
    'Коридор администрации Авроры': ('Медный провод',),
    'Кабинет администрации Авроры': ('Микросхема',),
    'Обрушенный коридор Авроры к грузовому отсеку': ('Стекло',),
    'Коридор грузового отсека Авроры': ('Медная руда',),
    'Главный грузовой отсек Авроры': ('Титан',),
    'Грузовой подъёмник Авроры': ('Титановый слиток',),
    'Нижний грузовой отсек Авроры': ('Сталь',),
    'Четырёхсторонний коридор Авроры': ('Металлолом',),
    'Отсек «Мотылёк» Авроры': (
        'Модуль глубины «Мотылька» – Мод. 1',
        'Солнечная зарядка «Мотылька»',
        'Гидролокатор «Мотылька»'),
    'Коридор двигательного отсека Авроры': ('Комплект проводов',),
    'Двигательный отсек / реактор Авроры': ('Топливный стержень реактора',),
    'Раздевалка Авроры': ('Обычный кислородный баллон',),
    # Оружейная зона: большую часть составляют коробки патронов.
    'Отсек костюмов КРАБ / PRAWN Bay Авроры': (
        'Коробка патронов (20)', 'Коробка патронов (20)',
        'Коробка патронов (20)', 'Коробка патронов (20)',
        'Коробка патронов (20)', 'Коробка патронов (20)',
        'Коробка патронов (20)', 'Коробка патронов (20)',
        'Энергозаряды (20)', 'Энергозаряды (20)',
        'Подводное ружьё', 'Плазменная пушка',
        'Дальняя пушка', 'Тяжёлая пушка'),
    'Пролом корпуса PRAWN Bay Авроры': ('Порох',),
    'Жилой коридор Авроры': ('Аптечка',),
    'Столовая Авроры': ('Сухой паёк Авроры', 'Бутылка воды Авроры'),
    'Каюта №1 Авроры': ('Кварц',),
    'Каюта капитана Авроры': ('Ионная батарея',),
    'Каюта №3 Авроры': ('Золотая руда',),
    'Каюта №4 Авроры': ('Литий',),
    'Каюта №6 Авроры': ('Ремкомплект',),
    'Каюта №7 Авроры': ('Фонарик',),
    'Подводный технический проход Авроры': ('Энергоячейка',),
    'Верхняя комната чёрного ящика Авроры': ('Рубин',),
    'Нижняя комната чёрного ящика Авроры': ('Магнетит',),
    'Большая катушка данных Авроры': ('Ионный куб',),
    'Лаборатория Авроры': ('Сканер',),
    'Обрушенный коридор лаборатории Авроры': ('Алмаз',),
    'Выходной коридор Авроры': ('Свинец',),
}


def aurora_loot_for_room(name):
    """Без случайной смеси: ассортимент строго закреплён за отсеком."""
    return AURORA_ROOM_STOCK.get(name, ('Металлолом',))


def populate_aurora_room_loot(world):
    """Плотные одинарные ряды у стен, в просторных помещениях — в центре.

    Готовые места запоминаются, чтобы периодический респавн не разбрасывал
    предметы по комнате. Все клетки проверяются по реальному приоритету биомов.
    """
    registry = START_ITEM_REGISTRY
    grid = world.object_grid
    world.aurora_loot_slots = {}
    rooms = [b for b in world.biomes if getattr(b, 'aurora_loot_room', False)]
    for room_index, room in enumerate(rooms):
        x1, y1, x2, y2 = room.rect
        width, height = x2 - x1 + 1, y2 - y1 + 1
        mid_x, mid_y = (x1 + x2) // 2, (y1 + y2) // 2
        names = aurora_loot_for_room(room.name)
        pool = [registry[n] for n in names
                if n in registry and isinstance(registry[n], ObjectType)
                and not registry[n].is_buildable]
        if not pool:
            continue
        rng = random.Random(31102026 + room_index * 1091 + _bio_stable_seed(room.name))
        rng.shuffle(pool)

        def usable(x, y):
            if not (x1 < x < x2 and y1 < y < y2):
                return False
            if (x, y) in grid or world.get_biome_at(x, y) is not room:
                return False
            # Пустое пространство около дверей и осевые проходы.
            if (y in (y1 + 1, y2 - 1) and abs(x - mid_x) <= 3):
                return False
            if (x in (x1 + 1, x2 - 1) and abs(y - mid_y) <= 3):
                return False
            # Зона спавна героя в каюте №6 остаётся незаставленной.
            if 790 <= x <= 796 and 632 <= y <= 638:
                return False
            return True

        candidates = []
        # Ровно один ряд возле каждого края, интервал 1 пустая клетка.
        for x in range(x1 + 2, x2 - 1, 2):
            candidates.extend(((x, y1 + 1), (x, y2 - 1)))
        for y in range(y1 + 2, y2 - 1, 2):
            candidates.extend(((x1 + 1, y), (x2 - 1, y)))

        if width >= 23 and height >= 18:
            row_offset = max(4, min(10, height // 5))
            # Два внутренних ряда; у особо крупных — ещё третий.
            lanes = [mid_y - row_offset, mid_y + row_offset]
            if width >= 46 and height >= 32:
                lanes.insert(1, mid_y)
            for y in lanes:
                for x in range(x1 + 4, x2 - 3, 4):
                    if abs(x - mid_x) <= 2 or abs(y - mid_y) <= 2:
                        continue
                    candidates.append((x, y))

        placed = 0
        seen = set()
        for x, y in candidates:
            if (x, y) in seen:
                continue
            seen.add((x, y))
            if not usable(x, y):
                continue
            item = pool[placed % len(pool)]
            grid[x, y] = item
            world.aurora_loot_slots.setdefault((room, item), []).append((x, y))
            placed += 1


_original_world_generate_with_resources = World._generate_objects


def generate_objects_with_aurora_loot(self):
    _original_world_generate_with_resources(self)
    populate_aurora_room_loot(self)


World._generate_objects = generate_objects_with_aurora_loot


# Столовая получает настоящие съедобные припасы, а не строительную химию.
_original_init_assets_before_aurora_food = init_assets


def init_assets_with_aurora_food():
    _original_init_assets_before_aurora_food()
    food = ObjectType('Сухой паёк Авроры', (198, 158, 91), 23, 'rect',
                      edible=True, hunger_value=45, thirst_value=2)
    water = ObjectType('Бутылка воды Авроры', (95, 181, 218), 22, 'rect',
                       edible=True, hunger_value=0, thirst_value=35)
    for item in (food, water):
        item.image = generate_tech_item_sprite(item.name, item.size, item.color)
        START_ITEM_REGISTRY[item.name] = item


init_assets = init_assets_with_aurora_food

# Повторное появление ресурсов — только на исходных местах ряда.
_original_respawn_aurora_resource = Game._spawn_missing_surface_resource


def respawn_aurora_stock_at_designated_slot(self, biome, object_type):
    if getattr(biome, 'aurora_loot_room', False):
        slots = getattr(self.world, 'aurora_loot_slots', {}).get((biome, object_type), ())
        for x, y in slots:
            cell = (x, y)
            if (cell not in self.world.object_grid
                    and not self.surface_dropped_items.get(cell)
                    and self.world.get_biome_at(x, y) is biome):
                self.world.object_grid[cell] = object_type
                return True
        return False
    return _original_respawn_aurora_resource(self, biome, object_type)


Game._spawn_missing_surface_resource = respawn_aurora_stock_at_designated_slot
# <<< AURORA_SPECIALIZED_STOCK_V2 <<<


# >>> DESTRUCTIBLE_WALLS_V1 >>>
# Разрушаемые металлические стены Авроры, стены построек и капсулы.
# Прочность хранится по клеткам, а не в объекте типа стены:
# выстрел по одному сегменту не повреждает соседние сегменты.
AURORA_PARTITION_HP = 90
AURORA_HULL_HP = 180
PLAYER_WALL_HP = 65
CAPSULE_WALL_HP = 110

# Фоновая металлическая панель для клеток, где больше нет корпуса.
AURORA_BREACH_BIOME = Biome(
    'Пролом корпуса Авроры', (69, 73, 76), 0, 0, 0, 0,
    [], [], passable=True, priority=100)
AURORA_BREACH_BIOME.aurora_style = True
AURORA_BREACH_BIOME.location_kind = 'aurora_wreck'

_original_biome_lookup_before_damage = World.get_biome_at


def biome_lookup_with_aurora_breaches(self, x, y):
    key = (int(x), int(y))
    breach = getattr(self, 'aurora_breaches', None)
    if breach is not None and key in breach:
        return breach[key]
    return _original_biome_lookup_before_damage(self, x, y)


World.get_biome_at = biome_lookup_with_aurora_breaches

_original_capsule_wall_check = Game.is_start_capsule_wall


def capsule_wall_can_be_breached(self, x, y):
    if (int(x), int(y)) in getattr(self, 'capsule_breaches', ()):
        return False
    return _original_capsule_wall_check(self, x, y)


Game.is_start_capsule_wall = capsule_wall_can_be_breached


def _wall_hit_cell(game, sx, sy, dx, dy, t):
    """Find the blocked tile *after* a DDA entry, including corner hits."""
    pt = min(1., max(0., t + 0.00001))
    x, y = math.floor(sx + dx * pt), math.floor(sy + dy * pt)
    candidates = ((x, y), (x + (1 if dx > 0 else -1), y),
                  (x, y + (1 if dy > 0 else -1)))
    for cx, cy in candidates:
        if not (0 <= cx < MAP_SIZE and 0 <= cy < MAP_SIZE):
            continue
        ob = game.world.object_grid.get((cx, cy))
        if ob is not None and ob.is_wall:
            return cx, cy, 'building', ob
        if game.is_start_capsule_wall(cx, cy):
            return cx, cy, 'capsule', None
        b = game.world.get_biome_at(cx, cy)
        if b is not None and not b.passable and getattr(b, 'aurora_style', False):
            return cx, cy, 'aurora', b
    return None


def _damage_wall_at_shot_contact(game, shot, sx, sy, dx, dy, t):
    """Damage the impacted wall; never destroy arbitrary terrain outside walls."""
    # Каменные/полигональные границы пещер остаются нетронутыми,
    # но построенную внутри пещеры стену можно прострелить.
    weapon = str(shot.get('weapon_name', ''))
    vehicle_kind = shot.get('weapon_kind')
    is_cannon = ('пушка' in weapon.lower() or vehicle_kind == 'heavy'
                 or ('weapon_kind' not in shot and 'weapon_name' not in shot))
    if not is_cannon:
        return
    if game.in_cave:
        pt = min(1., max(0., t + 0.00001))
        x = math.floor(sx + dx * pt)
        y = math.floor(sy + dy * pt)
        thing = game.cave_object_grid.get((x, y))
        if thing is None or not thing.is_wall:
            return
        material = 'cave_building'
        collided = (x, y, material, thing)
    else:
        collided = _wall_hit_cell(game, sx, sy, dx, dy, t)
    if collided is None:
        return
    x, y, material, thing = collided
    key = (x, y)
    # Тяжёлые снаряды наиболее эффективны, пулемёт не пробивает корпус.
    multiplier = 1.7 if vehicle_kind == 'heavy' else 1.25
    damage = max(1, round(float(shot.get('damage', 0)) * multiplier))
    if material == 'aurora':
        hp_map = getattr(game.world, 'aurora_wall_hp', None)
        if hp_map is None:
            hp_map = game.world.aurora_wall_hp = {}
        if 'Корпус' in thing.name or 'Нос' in thing.name:
            max_hp = AURORA_HULL_HP
        else:
            max_hp = AURORA_PARTITION_HP
    elif material in ('building', 'cave_building'):
        owner = game if material == 'cave_building' else game.world
        hp_map = getattr(owner, 'constructed_wall_hp', None)
        if hp_map is None:
            hp_map = owner.constructed_wall_hp = {}
        max_hp = PLAYER_WALL_HP
    else:
        hp_map = getattr(game, 'capsule_wall_hp', None)
        if hp_map is None:
            hp_map = game.capsule_wall_hp = {}
        max_hp = CAPSULE_WALL_HP
    remaining = max(0, hp_map.get(key, max_hp) - damage)
    hp_map[key] = remaining
    if remaining > 0:
        game.notification = f'Стена: {remaining}/{max_hp} HP'
        game.notification_timer = 32
        return
    if material in ('building', 'cave_building'):
        # Only remove if the block really is still a wall.
        grid = (game.cave_object_grid if material == 'cave_building'
                else game.world.object_grid)
        if grid.get(key) is thing:
            del grid[key]
        hp_map.pop(key, None)
    elif material == 'capsule':
        game.capsule_breaches = getattr(game, 'capsule_breaches', set())
        game.capsule_breaches.add(key)
        hp_map.pop(key, None)
    else:
        # Seek the topmost passable metal surface below the broken wall.
        candidates = game.world.biome_chunks.get(
            (x // WORLD_CHUNK_SIZE, y // WORLD_CHUNK_SIZE), ())
        floor = next((b for b in candidates
                      if b.passable and getattr(b, 'aurora_style', False)
                      and b.contains(x, y)), AURORA_BREACH_BIOME)
        game.world.aurora_breaches = getattr(game.world, 'aurora_breaches', {})
        game.world.aurora_breaches[key] = floor
        hp_map.pop(key, None)
    game.notification = 'Стена уничтожена — проход открыт!'
    game.notification_timer = 75


_original_draw_aurora_cell = Game.draw_aurora_cell


def draw_aurora_with_wall_damage(self, x, y, sx, sy, biome):
    _original_draw_aurora_cell(self, x, y, sx, sy, biome)
    key = (x, y)
    if key in getattr(self.world, 'aurora_breaches', ()):
        # Прожжённый край пробоины, без создания отдельных изображений.
        pygame.draw.rect(self.screen, (48, 42, 42), (sx + 2, sy + 2, CELL_SIZE - 4, CELL_SIZE - 4), 2)
        pygame.draw.line(self.screen, (204, 118, 61), (sx + 5, sy + 5), (sx + 13, sy + 10), 2)
        pygame.draw.line(self.screen, (45, 39, 36), (sx + 9, sy + 22), (sx + 21, sy + 13), 2)
    elif key in getattr(self.world, 'aurora_wall_hp', ()):
        # Трещины появляются только после повреждения.
        pygame.draw.line(self.screen, (22, 28, 31), (sx + 4, sy + 5), (sx + 16, sy + 17), 2)
        pygame.draw.line(self.screen, (25, 29, 33), (sx + 16, sy + 17), (sx + 26, sy + 12), 2)
        pygame.draw.line(self.screen, (215, 142, 81), (sx + 8, sy + 21), (sx + 17, sy + 17), 1)


Game.draw_aurora_cell = draw_aurora_with_wall_damage

_original_draw_capsule_cell = Game.draw_start_capsule_cell


def draw_capsule_with_wall_damage(self, x, y, sx, sy):
    if (x, y) in getattr(self, 'capsule_breaches', ()):
        # Открытый проход в капсуле: рисуется тот же настил, что в помещении.
        pygame.draw.rect(self.screen, (41, 67, 78), (sx, sy, CELL_SIZE, CELL_SIZE))
        pygame.draw.rect(self.screen, (89, 125, 133), (sx + 4, sy + 4, CELL_SIZE - 8, CELL_SIZE - 8), 1)
        return True
    result = _original_draw_capsule_cell(self, x, y, sx, sy)
    if (x, y) in getattr(self, 'capsule_wall_hp', ()):
        pygame.draw.line(self.screen, (25, 32, 35), (sx + 4, sy + 5), (sx + 23, sy + 20), 2)
    return result


Game.draw_start_capsule_cell = draw_capsule_with_wall_damage
# <<< DESTRUCTIBLE_WALLS_V1 <<<



# >>> FAST_BIOME_MAP_AND_HUMOR_V1 >>>
# Индексированная статистика: не делаем тысячи get_biome_at на каждый щелчок.
# При каждой операции изменения словаря корректируется ровно один счётчик.
class BiomeCountGrid(dict):
    """Обычный словарь координат с актуальным счётчиком предметов по биомам."""

    def __init__(self, initial, biome_lookup, name_of):
        super().__init__(initial)
        self.biome_lookup = biome_lookup
        self.name_of = name_of
        self.biome_counts = {}
        self._build_counts()

    def _build_counts(self):
        from collections import Counter
        self.biome_counts.clear()
        for key, value in self.items():
            biome = self.biome_lookup(*key)
            name = self.name_of(value)
            if biome is not None and name:
                self.biome_counts.setdefault(biome, Counter())[name] += 1

    def _adjust(self, key, value, delta):
        from collections import Counter
        biome = self.biome_lookup(*key)
        name = self.name_of(value)
        if biome is None or not name:
            return
        counts = self.biome_counts.setdefault(biome, Counter())
        counts[name] += delta
        if counts[name] <= 0:
            del counts[name]
        if not counts:
            self.biome_counts.pop(biome, None)

    def __setitem__(self, key, value):
        if key in self:
            self._adjust(key, super().__getitem__(key), -1)
        super().__setitem__(key, value)
        self._adjust(key, value, 1)

    def __delitem__(self, key):
        value = super().__getitem__(key)
        super().__delitem__(key)
        self._adjust(key, value, -1)

    def pop(self, key, *default):
        if key in self:
            value = super().pop(key)
            self._adjust(key, value, -1)
            return value
        if default:
            return default[0]
        raise KeyError(key)

    def popitem(self):
        key, value = super().popitem()
        self._adjust(key, value, -1)
        return key, value

    def setdefault(self, key, default=None):
        if key not in self:
            self[key] = default
        return self[key]

    def update(self, other=(), **kwargs):
        for key, value in dict(other, **kwargs).items():
            self[key] = value

    def __ior__(self, other):
        self.update(other)
        return self

    def clear(self):
        super().clear()
        self.biome_counts.clear()


def _map_grid_item_name(obj):
    return getattr(obj, 'name', None)


def _map_grid_tree_name(tree):
    try:
        return TREE_SPECIES[tree[0]]
    except (TypeError, ValueError, IndexError, KeyError):
        return None


def _install_biome_map_index(self):
    """Один раз после генерации; автоматический учёт всех новых находок."""
    surface_lookup = self.world.get_biome_at
    cave_lookup = self.get_cave_tunnel_at
    self.world.object_grid = BiomeCountGrid(
        self.world.object_grid, surface_lookup, _map_grid_item_name)
    self.world.flora_grid = BiomeCountGrid(
        self.world.flora_grid, surface_lookup, _map_grid_item_name)
    self.world.tree_grid = BiomeCountGrid(
        self.world.tree_grid, surface_lookup, _map_grid_tree_name)
    self.cave_object_grid = BiomeCountGrid(
        self.cave_object_grid, cave_lookup, _map_grid_item_name)
    self.cave_flora_grid = BiomeCountGrid(
        self.cave_flora_grid, cave_lookup, _map_grid_item_name)
    # CaveWorldProxy должен использовать тот же индексируемый словарь.
    self.cave_world.object_grid = self.cave_object_grid
    self._map_info_render_cache = None


Game._install_biome_map_index = _install_biome_map_index

BIOME_MAP_LORE = {
    'Мелководье': (
        'Тут красиво, пока кто-нибудь не объявит водоросль начальником экспедиции.',
        'Обычно спокойно, но проверяйте неожиданных гостей и освещение.'),
    'Глубоководье': (
        'Каждый метр глубины бесплатный. Возвращение на поверхность — уже нет.',
        'Тьма и потеря ориентиров; флору лучше сначала изучить.'),
    'Центральное море': (
        'Море занимает центр мироздания и уверено, что вы пришли выполнять поручения.',
        'ЧБОС-кит выдаёт опасные задания; отказы не принимаются.'),
    'Побережье': (
        'Песок, ветер и местные жители, уверенные, что это суша навсегда.',
        'Акласуры могут ударить ногой; следите за сухопутными големами.'),
    'Острова': (
        'Кусочки камня, официально объявившие себя независимыми государствами.',
        'Големы и акласуры; опаснее всего считать их декорациями.'),
    'Коралловый риф': (
        'Кораллы старательно изображают курорт, а рыбы требуют визу для въезда.',
        'Живность и тесные участки рифа; ночью ориентироваться сложнее.'),
    'Леса': (
        'Деревья растут быстрее, чем исследователь придумывает их названия.',
        'Бешеный мох, големогенные колонии и наземные сюрпризы.'),
    'Кельповый лес': (
        'Морская лапша длиной в много километров. Соус не предусмотрен.',
        'Заросли ограничивают обзор; внимательно осматривайте флору.'),
    'Био-люминесцентная зона': (
        'Здесь даже темнота подсвечена, чтобы было видно, насколько вам страшно.',
        'Ночной мрак и необычная светящаяся флора.'),
    'Горы': (
        'Бесплатный альпинизм по камням с непредсказуемыми санитарными нормами.',
        'Каменные големы; при встрече с живым валуном не спорьте о геологии.'),
    'Озёра (внешние)': (
        'Маленькое море, которое не прошло собеседование на большой океан.',
        'Скрытые под водой угрозы и водяные големы.'),
    'Реки': (
        'Течение знает короткий путь и никому его не рассказывает.',
        'Узкие проходы, водные существа и неудачные повороты.'),
    'Притоки': (
        'Служебный вход реки; туристы тут обычно только по ошибке.',
        'Тесная вода; не забывайте про выход.'),
    'Водопады': (
        'Самая громкая водопроводная авария на астероиде.',
        'Сложно ориентироваться возле перепадов и узких русел.'),
}


def _map_biome_description(biome, cave):
    name = biome.name
    if cave:
        if 'развилка' in name.lower():
            return ('Подземная развилка: право, лево и ещё один неправильный поворот.',
                    'Пещерная фауна, возможные узкие проходы и плохая видимость.')
        if 'выходной тоннель' in name or 'Межбиомный тоннель' in name:
            return ('Тоннель создан, чтобы до конца пути вы начали ценить открытое пространство.',
                    'Узкие участки и пещерные обитатели; держите путь к выходу.')
        return ('Пещерный зал: потолок имеется, гарантия от падения камней не прилагается.',
                'Тёмные закоулки и встреча с подводными соседями.')
    if getattr(biome, 'aurora_style', False):
        if getattr(biome, 'aurora_loot_room', False):
            stocked = AURORA_ROOM_STOCK.get(name, ())
            kinds = ', '.join(dict.fromkeys(stocked[:3])) if stocked else 'металлолом'
            return (f'Отдел снабжения сообщает: складируем {kinds}. Всё остальное испарилось.',
                    'Металлические перегородки можно пробить пушками; смотрите под ноги.')
        if not biome.passable:
            return ('Железная стена: молчаливая и очень сопротивляется перестройке.',
                    'Проход закрыт, пока корпус не пробит подходящим орудием.')
        return ('Аврора: построена с размахом, разобрана с энтузиазмом.',
                'Повреждённые проходы и обломки; пушки меняют архитектуру.')
    return BIOME_MAP_LORE.get(name, (
        'Местность не описана в путеводителе. Вероятно, путеводитель не вернулся.',
        'Проверьте местную фауну и выходы до наступления темноты.'))


def _map_danger_report(biome, fauna, cave):
    """Оценка основана на реально зарегистрированной в панели фауне."""
    dangers = []
    score = 1 if biome.passable else 4
    for name, count in fauna.items():
        if count <= 0:
            continue
        if name == WHALE_NAME:
            dangers.append(f'{name} ({count}): выдаёт смертельные квесты')
            score = max(score, 10)
        elif name == 'Морской дракон-левиафан':
            dangers.append(f'Морской дракон ({count}): огненные шары')
            score = max(score, 10)
        elif name == 'Пиявка-гидра':
            dangers.append(f'Пиявка-гидра ({count}): пьёт кровь щупальцами')
            score = max(score, 8)
        elif name == 'Птица Говносрун':
            dangers.append(f'Птица Говносрун ({count}): артобстрел')
            score = max(score, 9)
        elif name == 'АКЛАСУР':
            dangers.append(f'Акласур ({count}): укусы и удары ногами')
            score = max(score, 7)
        elif 'голем' in name.lower() and 'бактерии' not in name.lower():
            dangers.append(f'{name} ({count}): бьётся и может собраться заново')
            score = max(score, 6)
        elif 'бактерии' in name.lower():
            dangers.append(f'Колонии големогенных бактерий ({count})')
            score = max(score, 4)
    if cave:
        score = max(score, 3)
    if getattr(biome, 'aurora_style', False) and not biome.passable:
        score = max(score, 5)
    return (min(10, score), dangers)


def _build_biome_panel_lines(self, biome, counts, width):
    resources, flora, fauna = counts
    cave = self.in_cave and biome in self.cave_tunnels
    lines = []
    default_color = (221, 231, 227)

    def add(text='', color=default_color, bold=False, wrap=True):
        font = self.small_font if bold else self.tiny_font
        if not text:
            lines.append(('', color, bold))
            return
        if not wrap or font.size(text)[0] <= width:
            lines.append((text, color, bold))
            return
        for paragraph in text.split('\n'):
            row = ''
            for word in paragraph.split():
                test = word if not row else row + ' ' + word
                if font.size(test)[0] <= width:
                    row = test
                else:
                    if row:
                        lines.append((row, color, bold))
                    row = word
                    # Переносим необычно длинные составные слова по символам.
                    while font.size(row)[0] > width and len(row) > 1:
                        cut = len(row) - 1
                        while cut > 1 and font.size(row[:cut])[0] > width:
                            cut -= 1
                        lines.append((row[:cut], color, bold))
                        row = row[cut:]
            if row:
                lines.append((row, color, bold))

    add('Найдены на этом участке:', (194, 230, 226), True)
    add('Реальные объекты и находки', (169, 187, 182))
    add()
    for heading, data, color in (
        ('РАСТЕНИЯ', flora, (143, 240, 151)),
        ('СУЩЕСТВА', fauna, (255, 187, 150)),
        ('РЕСУРСЫ', resources, (175, 212, 244))):
        add(f'{heading} ({sum(data.values())})', color, True)
        if data:
            for species, count in sorted(data.items(), key=lambda t: (-t[1], t[0])):
                add(f'{species}: {count}')
        else:
            add('Не обнаружено', (142, 153, 150))
        add()
    # Именно после перечня ресурсов располагается описание.
    add('О БИОМЕ — ОТЧЁТ ЭКСПЕДИЦИИ', (245, 211, 104), True)
    flavor, warning = _map_biome_description(biome, cave)
    add(flavor, (229, 215, 175))
    add()
    add('ПАРАМЕТРЫ', (169, 225, 240), True)
    if cave:
        environment = 'Пещера / тоннель'
        depth = 'Подземный уровень'
    elif getattr(biome, 'aurora_style', False):
        environment = 'Отсек корабля' if biome.passable else 'Стена корабля'
        depth = 'Внутри Авроры'
    elif is_land_biome(biome):
        environment, depth = 'Суша', 'Не под водой'
    else:
        environment, depth = 'Вода', (getattr(biome, 'depth_label', None) or 'Не задана')
    add('Среда: ' + environment)
    add('Глубина: ' + depth)
    add(f'Площадь контура: ~{int(biome.area()):,} кл.'.replace(',', ' '))
    add('Проходимость: ' + ('да' if biome.passable else 'нет'))
    add(f'Ресурсных предметов: {sum(resources.values())}')
    add(f'Видов флоры / фауны: {len(flora)} / {len(fauna)}')
    risk, dangers = _map_danger_report(biome, fauna, cave)
    add(f'Опасность (условно): {risk}/10',
        (255, 129, 113) if risk >= 7 else (250, 211, 137) if risk >= 4 else (155, 229, 167), True)
    add()
    add('ОПАСНОСТИ', (247, 154, 133), True)
    add(warning, (239, 205, 191))
    if dangers:
        for threat in dangers[:6]:
            add('• ' + threat, (255, 183, 163))
        if len(dangers) > 6:
            add(f'И ещё {len(dangers)-6} угрожающих разновидностей.')
    else:
        add('Опасных существ сейчас не обнаружено. Это не лицензия на беспечность.',
            (163, 218, 174))
    add()
    add('Колесо: прокрутка | ЛКМ: выбор', (151, 184, 180))
    return lines


Game._build_biome_panel_lines = _build_biome_panel_lines
# <<< FAST_BIOME_MAP_AND_HUMOR_V1 <<<

# >>> PROраб_САМОСТОЯТЕЛЬНАЯ_БРИГАДА_V2 >>>
# Все рабочие живут постоянно: расстояние до игрока НИКОГДА не влияет
# на скорость их перемещения, добычи, голод или здоровье.
from collections import Counter, deque
import heapq

CREW_SIZE = 100
CREW_WALK_SPEED = 0.115
CREW_PROFESSIONS = ('добытчик', 'строитель', 'инженер', 'технолог', 'исследователь', 'охотник', 'снабженец')
CREW_PRODUCTS = (
    ('Аптечка', 22), ('Сканер', 16), ('Ремонтный инструмент', 16),
    ('Строитель', 22), ('Подводное ружьё', 25), ('Лазерный пистолет', 20),
    ('Термоклинок', 18), ('Грави-ловушка', 16), ('Фонарик', 15),
    ('Морской глайдер', 12), ('Коробка патронов (20)', 15),
    ('Ремкомплект', 12), ('Лёгкий джип', 5), ('Бронетранспортёр', 3),
    ('Мотылёк', 7), ('Костюм КРАБ', 5)
)

class CrewBuilding:
    def __init__(self, name, kind, pos, built=False, needs=None):
        self.name = name
        self.kind = kind
        self.x, self.y = pos
        self.built = built
        self.needs = Counter(needs or {})
        self.progress = 100.0 if built else 0.0
        self.stock = Counter()
        self.work = 0
        self.reserved = False

    def ready(self):
        return all(self.stock[obj] >= n for obj,n in self.needs.items())


class CrewWorker:
    def __init__(self, number, x, y):
        self.number = number
        self.x, self.y = float(x)+0.5, float(y)+0.5
        self.role = CREW_PROFESSIONS[(number * 5 + number // 9) % len(CREW_PROFESSIONS)]
        self.hp = 25.0
        self.food = 100.0
        self.water = 100.0
        self.alive = True
        self.weapon = START_ITEM_REGISTRY['Нож']
        self.tools = set()
        self.vehicle = None
        self.vehicle_hp = 0
        self.ammo = 0
        self.pack = []
        self.action = 'патрулирует'
        self.goal = (x,y)
        self.target = None
        self.route = deque()
        self.stuck = 0
        self.rethink = 0
        self.activity = 0.0
        self.combat_timer = 0
        self.damage_timer = 0
        self.direction = 1
        self.movement = 0.0
        self.scans = 0


class ForemanCrew:
    """100 постоянно обновляемых NPC. Предметы и рецепты — настоящие объекты игры."""
    def __init__(self, game):
        self.game = game
        self.workers = []
        self.buildings = []
        self.stats = Counter()
        self.order = 'авто'
        self.frame = 0
        self.recipes = {recipe.result.name: recipe for recipe in recipes}
        self.food_chunks = {}
        for (x,y),item in self.game.world.object_grid.items():
            if isinstance(item,ObjectType) and item.edible:
                self.food_chunks.setdefault((x//16,y//16),set()).add((x,y))
        self._spawn()
        self._create_buildings()

    def _spawn(self):
        rooms = [r for r in self.game.world.biomes if getattr(r,'aurora_loot_room',False)
                 and (r.name == 'Раздевалка Авроры' or r.name.startswith('Каюта '))]
        locker = next(r for r in rooms if r.name=='Раздевалка Авроры')
        cabins = sorted((r for r in rooms if r is not locker), key=lambda r:r.name)
        rng = random.Random(20261031)
        for room, amount in [(locker, 30)]+[(r,70//len(cabins)+int(i<70%len(cabins)))
                                                for i,r in enumerate(cabins)]:
            x1,y1,x2,y2=room.rect
            candidates=[(x,y) for y in range(y1+1,y2) for x in range(x1+1,x2)
                        if (self.game.world.get_biome_at(x,y) or room).passable
                        and (x,y)!=(793,635) and
                        not getattr(self.game.world.get_object_at(x,y),'is_wall',False)]
            rng.shuffle(candidates)
            if len(candidates)<amount:
                raise RuntimeError('Недостаточно мест для рабочих: '+room.name)
            for x,y in candidates[:amount]:
                self.workers.append(CrewWorker(len(self.workers)+1,x,y))
        assert len(self.workers)==CREW_SIZE

    def _create_buildings(self):
        obj = START_ITEM_REGISTRY
        for name,kind,position,built,recipe in (
            ('Центральный склад Авроры','склад',(781,472),True,{}),
            ('Заводской цех Авроры','завод',(794,586),True,{}),
            ('Лаборатория Авроры','лаборатория',(838,620),True,{}),
            ('Новый склад','склад',(780,479),False,{'Титан':8,'Металлолом':3}),
            ('Сборочный цех','завод',(791,595),False,{'Титан':8,'Медная руда':3}),
            ('Исследовательский корпус','лаборатория',(835,625),False,{'Титан':6,'Кварц':3}),
        ):
            self.buildings.append(CrewBuilding(name,kind,position,built,
                                  {obj[k]:v for k,v in recipe.items() if k in obj}))
        self.store, self.factory, self.lab = self.buildings[:3]
        # Внутрикорабельная сеть складских конвейеров: груз сдаётся в ближайшей
        # каюте/раздевалке, а изготовитель получает его из общего учтённого запаса.
        self.factory.stock = self.store.stock
        self.drop_points = [(int((b.rect[0]+b.rect[2])/2),int((b.rect[1]+b.rect[3])/2))
                            for b in self.game.world.biomes
                            if getattr(b,'aurora_loot_room',False)
                            and (b.name.startswith('Каюта ') or b.name=='Раздевалка Авроры')]
        self.drop_points.append((self.factory.x,self.factory.y))

    def _depot(self,w):
        return min(self.drop_points,key=lambda p:(p[0]-w.x)**2+(p[1]-w.y)**2)

    def _passable(self,x,y):
        if not (1<=x<MAP_SIZE-1 and 1<=y<MAP_SIZE-1):return False
        b=self.game.world.get_biome_at(x,y)
        if b is None or not b.passable:return False
        if self.game.is_start_capsule_wall(x,y):return False
        o=self.game.world.get_object_at(x,y)
        return not getattr(o,'is_wall',False)

    def _path(self,w,dest):
        # A* с бюджетом; при большой дистанции ищем промежуточную точку,
        # поэтому никогда не просчитываем сразу всю карту для 100 работников.
        start=(int(w.x),int(w.y)); end=(int(dest[0]),int(dest[1]))
        d=max(abs(end[0]-start[0]),abs(end[1]-start[1]))
        if d>19:
            factor=18/d
            end=(int(start[0]+(end[0]-start[0])*factor),
                 int(start[1]+(end[1]-start[1])*factor))
        if not self._passable(*end):
            neighbours=[(end[0]+dx,end[1]+dy) for dx in range(-3,4) for dy in range(-3,4)]
            neighbours=[p for p in neighbours if self._passable(*p)]
            if not neighbours:return deque()
            end=min(neighbours,key=lambda p:(p[0]-end[0])**2+(p[1]-end[1])**2)
        if start==end:return deque()
        fringe=[(abs(end[0]-start[0])+abs(end[1]-start[1]),0,start)]
        parents={}; distance={start:0}; count=0
        while fringe and count<700:
            _,g,p=heapq.heappop(fringe)
            if g!=distance[p]:continue
            count+=1
            if p==end:
                path=[]
                while p!=start:
                    path.append(p);p=parents[p]
                return deque(reversed(path))
            x,y=p
            for q in ((x+1,y),(x-1,y),(x,y+1),(x,y-1)):
                if not self._passable(*q):continue
                ng=g+1
                if ng<distance.get(q,999999):
                    distance[q]=ng;parents[q]=p
                    h=abs(q[0]-end[0])+abs(q[1]-end[1])
                    heapq.heappush(fringe,(ng+h,ng,q))
        return deque()

    def _goal(self,w,p,action=None,target=None):
        p=(int(p[0]),int(p[1]))
        if w.goal!=p or (action is not None and action!=w.action):
            w.route.clear();w.stuck=0
        w.goal=p
        if action is not None:w.action=action
        if target is not None:w.target=target

    def _move(self,w):
        # Один и тот же темп ВСЕГДА, при любых расстояниях и у любой профессии.
        speed=CREW_WALK_SPEED  # строго постоянная скорость у всех рабочих
        if not w.route and math.hypot(w.x-(w.goal[0]+0.5),w.y-(w.goal[1]+0.5)) < 0.9:
            # Даже у станка рабочий постоянно ходит небольшими кругами.
            angle=self.frame*speed/0.30 + w.number*0.7
            nx=w.goal[0]+0.5+0.30*math.cos(angle)
            ny=w.goal[1]+0.5+0.30*math.sin(angle)
            if self._passable(int(nx),int(ny)):
                w.x,w.y=nx,ny
                w.movement+=speed
            return
        if not w.route and (w.rethink%16==0 or w.stuck==0):
            w.route=self._path(w,w.goal)
        if not w.route:
            w.stuck+=1
            # Периодический обход препятствий даже при ожидании материалов.
            if w.stuck>25:
                self._wander(w,5);w.stuck=0
            return
        px,py=w.route[0];tx,ty=px+0.5,py+0.5
        dx,dy=tx-w.x,ty-w.y;dist=math.hypot(dx,dy)
        if dist<=speed:
            w.x,w.y=tx,ty;w.route.popleft()
        else:
            nx=w.x+dx/dist*speed;ny=w.y+dy/dist*speed
            if self._passable(int(nx),int(ny)):
                w.x,w.y=nx,ny
            else:
                w.route.clear()
        w.direction=1 if dx>=0 else -1
        w.movement+=speed

    def _wander(self,w,radius=5):
        rng=random
        for _ in range(18):
            x=int(w.x)+rng.randint(-radius,radius)
            y=int(w.y)+rng.randint(-radius,radius)
            if self._passable(x,y):
                self._goal(w,(x,y),'патрулирует');return

    def _near_resource(self,w,food=False):
        g=self.game.world.object_grid
        x,y=int(w.x),int(w.y)
        best=None;score=1e9
        for xx in range(x-11,x+12):
            for yy in range(y-11,y+12):
                obj=g.get((xx,yy))
                if not isinstance(obj,ObjectType) or obj.is_wall or obj.is_buildable:continue
                if food and not obj.edible:continue
                d=(xx-w.x)**2+(yy-w.y)**2
                if d<score:
                    best=(xx,yy,obj);score=d
        if food and best is None:
            # Индекс съедобных ресурсов позволяет найти провиант за пределами
            # текущего экрана без сканирования всей карты на каждом кадре.
            cx,cy=x//16,y//16
            for radius in range(1,13):
                found=[]
                for bx in range(cx-radius,cx+radius+1):
                    for by in (cy-radius,cy+radius):
                        found.extend(self.food_chunks.get((bx,by),()))
                for by in range(cy-radius+1,cy+radius):
                    for bx in (cx-radius,cx+radius):
                        found.extend(self.food_chunks.get((bx,by),()))
                for xx,yy in found:
                    obj=g.get((xx,yy))
                    if obj is not None and obj.edible:
                        d=(xx-w.x)**2+(yy-w.y)**2
                        if d<score:best=(xx,yy,obj);score=d
                if best is not None:break
        return best

    def _near_animal(self,w,predator=False,edible=False,radius=6):
        animals=self.game.world.get_animals_near(w.x,w.y,radius)
        choices=[a for a in animals if a.hp>0 and not getattr(a,'is_companion',False)
                 and (not predator or a.type.behavior=='predator')
                 and (not edible or (a.type.edible and a.type.hp<=35))]
        if not choices:return None
        return min(choices,key=lambda a:(a.x-w.x)**2+(a.y-w.y)**2)

    def _use_food(self,w):
        # Пища и питьё — только из реально поднятых съедобных предметов/рыбы.
        for item in list(w.pack):
            if item.edible and (w.food<85 or w.water<85):
                w.food=min(100,w.food+item.hunger_value)
                w.water=min(100,w.water+item.thirst_value)
                w.pack.remove(item)
                w.action='ест '+item.name
                self.stats['съедено']+=1
                return True
        return False

    def _get_medkit(self,w):
        med=START_ITEM_REGISTRY.get('Аптечка')
        if med in w.pack:
            w.pack.remove(med);w.hp=min(25,w.hp+35)
            w.action='лечится аптечкой';self.stats['вылечено']+=1
            return True
        return False

    def _gear(self,w,item):
        """Распределение оружия и приборов, не исчезающих после использования."""
        name=item.name
        if name=='Коробка патронов (20)':
            w.ammo+=20;w.action='пополнил боезапас';return True
        if item.is_weapon:
            if w.weapon is None or item.damage>w.weapon.damage:
                old=w.weapon;w.weapon=item
                if old is not None and old.name!='Нож':w.pack.append(old)
                w.action='вооружился: '+name
                return True
        if name in ('Сканер','Строитель','Ремонтный инструмент','Фонарик',
                    'Грави-ловушка','Морской глайдер','Пропульсионная пушка',
                    'Стазис-винтовка','Лазерный резак'):
            if name not in w.tools:
                w.tools.add(name);w.action='оснастился: '+name
                return True
        if name in ('Лёгкий джип','Бронетранспортёр','Мотылёк','Костюм КРАБ','Циклоп'):
            if w.vehicle is None:
                w.vehicle=item
                w.vehicle_hp={'Лёгкий джип':15,'Бронетранспортёр':200,
                              'Мотылёк':50,'Костюм КРАБ':150,'Циклоп':400}.get(name,50)
                w.action='получил транспорт: '+name
                return True
        return False

    def _combat(self,w):
        enemy=self._near_animal(w,predator=True,radius=3.6)
        if enemy is None:return False
        d=math.hypot(enemy.x-w.x,enemy.y-w.y)
        w.action='защищается от '+enemy.type.name
        # Враги могут атаковать рабочих: это не декоративная симуляция.
        if d<1.5 and w.damage_timer<=0:
            damage=max(1,enemy.type.damage*0.42)
            if w.vehicle is not None and w.vehicle_hp>0:
                w.vehicle_hp=max(0,w.vehicle_hp-damage)
                if w.vehicle_hp==0:
                    w.vehicle=None
            else:
                w.hp=max(0,w.hp-damage)
            w.damage_timer=85
            self.stats['нападений хищников']+=1
        range_ = max(1.5,w.weapon.weapon_range)
        is_ranged=range_>2.0
        if w.combat_timer==0 and d<=(range_ if (not is_ranged or w.ammo>0) else 1.5):
            damage=max(2,w.weapon.damage if (not is_ranged or w.ammo>0) else 10)
            if is_ranged and w.ammo>0:w.ammo-=1
            enemy.hp-=damage
            w.combat_timer=30 if w.weapon.weapon_range<=2 else 48
            self.stats['отражено атак']+=1
            if enemy.hp<=0:
                self.game.world.remove_animal(enemy)
                if enemy.type.edible and len(w.pack)<6:w.pack.append(enemy.type)
                self.stats['убито хищников']+=1
        if w.hp<12 and d<2.8:
            x=int(w.x+(w.x-enemy.x)*3/max(d,0.1))
            y=int(w.y+(w.y-enemy.y)*3/max(d,0.1))
            if self._passable(x,y):self._goal(w,(x,y),'отступает')
        return True

    def _stocks(self):
        return self.factory.stock,self.store.stock

    def _recipe_chain(self,product,available,depth=0,seen=None):
        # Вернуть ближайший шаг настоящей производственной цепочки.
        if seen is None:seen=set()
        recipe=self.recipes.get(product)
        if recipe is None or depth>5 or product in seen:return None
        seen=seen|{product}
        shortages=[(item,n-available.get(item,0)) for item,n in recipe.ingredients.items()
                   if available.get(item,0)<n]
        if not shortages:return recipe
        for item,_ in shortages:
            next_step=self._recipe_chain(item.name,available,depth+1,seen)
            if next_step:return next_step
        return None

    def _craft_one(self,w):
        available,warehouse=self._stocks()
        # Фабрика/изготовитель, модификационная станция и сборщик транспорта.
        for name,quota in CREW_PRODUCTS:
            if self.stats['создано '+name]>=quota:continue
            if name=='Аптечка' and self.factory.stock[START_ITEM_REGISTRY.get('Аптечка')]>=12:continue
            recipe=self._recipe_chain(name,available)
            if recipe is None:continue
            station=recipe.station
            if station not in ('Изготовитель','Модификационная станция','Переносной сборщик транспорта'):
                continue
            if not recipe.can_craft(available):continue
            for item,count in recipe.ingredients.items():
                available[item]-=count
                if available[item]<=0:available.pop(item,None)
            available[recipe.result]+=recipe.result_count
            self.stats['создано '+recipe.result.name]+=recipe.result_count
            self.stats['всего изготовлено']+=recipe.result_count
            w.action='изготовил на '+station+': '+recipe.result.name
            return True
        return False

    def _needed_material(self):
        fab,warehouse=self._stocks()
        # Требуются также компоненты многоступенчатых рецептов.
        demand=Counter()
        for name,limit in CREW_PRODUCTS:
            if self.stats['создано '+name]>=limit:continue
            recipe=self.recipes.get(name)
            if recipe:
                for item,n in recipe.ingredients.items():
                    demand[item]=max(demand[item],n*2)
                    nested=self.recipes.get(item.name)
                    if nested:
                        for inner,qty in nested.ingredients.items():
                            demand[inner]=max(demand[inner],qty*2)
        # Для строительства нужны отдельные стройматериалы.
        for building in self.buildings:
            if not building.built:
                for obj,n in building.needs.items():
                    demand[obj]=max(demand[obj],n)
        return demand

    def _collect(self,w):
        desired=self._near_resource(w,food=(w.food<65 or w.water<65))
        if desired is None:desired=self._near_resource(w)
        if desired is None:return False
        self._goal(w,desired[:2],'собирает',desired)
        return True

    def _assign(self,w):
        if not w.alive:return
        w.target=None
        if w.hp<20 and self._get_medkit(w):return
        if w.food<78 or w.water<78:
            if self._use_food(w):return
            food=self._near_resource(w,food=True)
            if food:
                self._goal(w,food[:2],'ищет еду',food);return
            animal=self._near_animal(w,edible=True,radius=9)
            if animal is not None:
                self._goal(w,(animal.x,animal.y),'ловит еду',animal);return
        # Ремонт собственного транспорта — ремонтным прибором, без расхода HP.
        if (w.vehicle is not None and 'Ремонтный инструмент' in w.tools
                and w.vehicle_hp<{'Лёгкий джип':15,'Бронетранспортёр':200,
                                  'Мотылёк':50,'Костюм КРАБ':150,'Циклоп':400}.get(w.vehicle.name,50)):
            if w.rethink%40==0:
                w.vehicle_hp+=1;self.stats['ремонтов']+=1
                w.action='чинит транспорт ремонтным инструментом'
        # Медпункт: на базе аптечки из настоящих запасов.
        if w.hp<21 and self.factory.stock.get(START_ITEM_REGISTRY.get('Аптечка'),0)>0:
            self._goal(w,self._depot(w),'к аптечке');return
        if any(isinstance(i,AnimalType) for i in w.pack) and w.food>=75:
            self._goal(w,(self.lab.x,self.lab.y),'сдаёт образцы');return
        if self.order=='добыча':
            if self._collect(w):return
        # Снабжение рабочих: экипировка и приборы не выбрасываются в хранилище.
        # Рабочие забирают изготовленные орудия и устройства с цеха.
        preferred={'строитель':'Строитель','инженер':'Ремонтный инструмент',
                   'исследователь':'Сканер','охотник':'Грави-ловушка',
                   'добытчик':'Морской глайдер','технолог':'Сканер',
                   'снабженец':'Фонарик'}.get(w.role)
        available=self.factory.stock
        tool=START_ITEM_REGISTRY.get(preferred)
        if preferred and preferred not in w.tools and tool and available.get(tool,0)>0:
            self._goal(w,self._depot(w),'получает прибор');return
        # Для настоящего строительства материалы переносятся со склада на площадку.
        if not w.pack and (w.role in ('строитель','снабженец','инженер') or self.order=='строительство'):
            for building in self.buildings:
                if not building.built:
                    for obj,amount in building.needs.items():
                        if building.stock[obj]<amount and self.store.stock[obj]>0:
                            self._goal(w,self._depot(w),'забирает стройматериалы',(building,obj));return
        if len(w.pack)>=3:
            self._goal(w,self._depot(w),'сдаёт материалы');return
        if w.pack and (w.rethink%150<80):
            self._goal(w,self._depot(w),'сдаёт материалы');return
        # Цеху нужны ресурсы, которые рабочий переносит со склада.
        if w.role in ('снабженец','технолог','инженер') or self.order=='производство':
            wanted=self._needed_material()
            need=[o for o,n in wanted.items() if self.factory.stock[o]<n and self.store.stock[o]>0]
            if need:
                o=max(need,key=lambda obj:self.store.stock[obj]);
                self._goal(w,self._depot(w),'везёт в цех',o);return
        if w.role in ('технолог','инженер') or self.order=='производство':
            self._goal(w,(self.factory.x,self.factory.y),'работает на изготовителе');return
        if w.role in ('строитель','инженер') or self.order=='строительство':
            choices=[b for b in self.buildings if not b.built and (b.ready() or b.reserved)]
            if choices and 'Строитель' in w.tools:
                b=min(choices,key=lambda b:(w.x-b.x)**2+(w.y-b.y)**2)
                self._goal(w,(b.x,b.y),'строит помещение',b);return
        if w.role=='исследователь' and 'Сканер' in w.tools:
            a=self._near_animal(w,radius=7)
            if a:
                self._goal(w,(a.x,a.y),'сканирует',a);return
        if (w.role=='охотник' or self.order=='охота'):
            a=self._near_animal(w,edible=True,radius=9)
            if a:
                self._goal(w,(a.x,a.y),'ловит добычу',a);return
        if self._collect(w):return
        self._wander(w,8)

    def _at_task(self,w):
        act=w.action
        if act in ('собирает','ищет еду'):
            target=w.target
            if not isinstance(target,tuple):return
            x,y,item=target
            if self.game.world.object_grid.get((x,y)) is item:
                self.game.world.remove_object_at(x,y)
                if item.edible:
                    self.food_chunks.get((x//16,y//16),set()).discard((x,y))
                # Найденные оружие, приборы и транспорт рабочий оставляет себе.
                if not self._gear(w,item):w.pack.append(item)
                self.stats['добыто']+=1
                w.action='подобрал '+item.name
        elif act=='сдаёт образцы':
            for item in w.pack:self.lab.stock[item]+=1
            self.stats['образцов в лаборатории']+=len(w.pack)
            w.pack.clear()
        elif act=='получает прибор':
            self._supply(w)
        elif act=='забирает стройматериалы':
            building,obj=w.target
            qty=min(4,self.store.stock[obj],max(0,building.needs[obj]-building.stock[obj]))
            if qty:
                self.store.stock[obj]-=qty
                if self.store.stock[obj]<=0:self.store.stock.pop(obj,None)
                w.pack.extend([obj]*qty)
                self._goal(w,(building.x,building.y),'доставляет на стройку',building)
                return
        elif act=='доставляет на стройку':
            building=w.target
            for obj in w.pack:
                building.stock[obj]+=1
                self.stats['сырьё на стройке']+=1
            w.pack.clear()
        elif act=='сдаёт материалы':
            for item in w.pack:
                self.store.stock[item]+=1
                self.stats['в склад']+=1
            w.pack.clear()
        elif act=='везёт в цех':
            item=w.target
            if self.store.stock.get(item,0)>0:
                qty=min(5,self.store.stock[item])
                self.store.stock[item]-=qty
                if self.store.stock[item]<=0:self.store.stock.pop(item,None)
                w.pack.extend([item]*qty)
                self._goal(w,(self.factory.x,self.factory.y),'поставка на завод',item)
                return
        elif act=='поставка на завод':
            for item in w.pack:self.factory.stock[item]+=1
            self.stats['в производство']+=len(w.pack)
            w.pack.clear()
        elif act=='к аптечке':
            kit=START_ITEM_REGISTRY.get('Аптечка')
            if kit is not None and self.factory.stock[kit]>0:
                self.factory.stock[kit]-=1
                w.hp=min(25,w.hp+35)
                self.stats['вылечено']+=1
        elif act=='работает на изготовителе':
            self._craft_one(w)
        elif act=='строит помещение':
            b=w.target
            if b and not b.built and (b.ready() or b.reserved) and 'Строитель' in w.tools:
                if not b.reserved:
                    for obj,n in b.needs.items():b.stock[obj]-=n
                    b.reserved=True
                b.progress=min(100,b.progress+3)
                if b.progress>=100:
                    b.built=True
                    self.stats['построено']+=1
                    self.game.notification='Рабочие построили: '+b.name
                    self.game.notification_timer=100
        elif act in ('ловит еду','ловит добычу'):
            a=w.target
            if a is not None and a.hp>0 and a in self.game.world.animals:
                if math.hypot(w.x-a.x,w.y-a.y)<2.3:
                    # Ловушка помогает ловить добычу; без неё используется нож.
                    a.hp-=max(5,w.weapon.damage*0.8)
                    if a.hp<=0:
                        self.game.world.remove_animal(a)
                        if a.type.edible:w.pack.append(a.type)
                        self.stats['поймано']+=1
        elif act=='сканирует':
            a=w.target
            if a is not None and a.hp>0:
                w.scans+=1;self.stats['исследований']+=1
        self._wander(w,4)

    def _supply(self,w):
        # Готовые предметы цеха выдаются участникам бригады без бесплатного появления.
        stock=self.factory.stock
        if math.hypot(w.x-self._depot(w)[0],w.y-self._depot(w)[1])>4:return
        if w.hp<21 and stock.get(START_ITEM_REGISTRY.get('Аптечка'),0)>0:
            item=START_ITEM_REGISTRY['Аптечка'];stock[item]-=1
            w.hp=min(25,w.hp+35);self.stats['вылечено']+=1
        main={'строитель':'Строитель','инженер':'Ремонтный инструмент',
              'исследователь':'Сканер','охотник':'Грави-ловушка',
              'добытчик':'Морской глайдер','технолог':'Сканер',
              'снабженец':'Фонарик'}.get(w.role)
        equipment=('Подводное ружьё','Лазерный пистолет','Термоклинок',
                   'Коробка патронов (20)','Лёгкий джип','Мотылёк')
        for name in ((main,) if main else ()) + equipment:
            obj=START_ITEM_REGISTRY.get(name)
            if name=='Коробка патронов (20)' and w.ammo>=40:continue
            if obj is not None and stock.get(obj,0)>0 and self._gear(w,obj):
                stock[obj]-=1;self.stats['выдано снаряжения']+=1
                break

    def _build_supply(self,w):
        # Реальные материалы перебрасываются в строительную площадку.
        if not w.pack:return
        eligible=[b for b in self.buildings if not b.built and any(b.stock[o]<n for o,n in b.needs.items())]
        if not eligible:return
        b=min(eligible,key=lambda b:(b.x-w.x)**2+(b.y-w.y)**2)
        for obj in list(w.pack):
            if b.stock[obj]<b.needs[obj]:
                w.pack.remove(obj);b.stock[obj]+=1
                self.stats['сырьё на стройке']+=1

    def update(self):
        self.frame+=1
        for w in self.workers:
            if not w.alive:continue
            # Еда, жажда и здоровье обновляются постоянно, даже далеко от игрока.
            w.food=max(0,w.food-0.0021)
            w.water=max(0,w.water-0.0028)
            if w.food==0 or w.water==0:
                w.hp=max(0,w.hp-0.008)
            if w.hp<=0:
                w.alive=False;w.action='погиб';self.stats['потери']+=1
                continue
            w.rethink+=1
            if w.combat_timer>0:w.combat_timer-=1
            if w.damage_timer>0:w.damage_timer-=1
            if self.frame%13==w.number%13:
                if self._combat(w):
                    # Даже во время перестрелки рабочий не замирает.
                    if not w.route:self._wander(w,3)
            if self.frame%27==w.number%27:
                self._use_food(w)
            if self.frame%43==w.number%43:
                self._get_medkit(w)
                self._supply(w)
            if self.frame%100==w.number%100 or w.rethink>160:
                # Доставку нельзя отменять посреди маршрута.
                if w.action not in ('поставка на завод','доставляет на стройку'):
                    w.rethink=0
                    self._assign(w)
            self._move(w)
            d=math.hypot((w.goal[0]+0.5)-w.x,(w.goal[1]+0.5)-w.y)
            if d<=1.1:
                w.activity+=1
                if w.activity>=38:
                    w.activity=0
                    self._at_task(w)
            else:w.activity=0
            if len(w.pack)>9:
                self._goal(w,self._depot(w),'сдаёт материалы')

    def draw(self,screen,camera_x,camera_y):
        if self.game.in_cave:return
        sw,sh=screen.get_size()
        for building in self.buildings:
            x=int((building.x-camera_x)*CELL_SIZE)
            y=int((building.y-camera_y)*CELL_SIZE)
            if not (-80<x<sw+80 and -80<y<sh+80):continue
            color={'склад':(179,138,69),'завод':(66,143,178),'лаборатория':(128,89,179)}[building.kind]
            rect=pygame.Rect(x-24,y-24,48,48)
            pygame.draw.rect(screen,(31,37,42),rect,border_radius=3)
            pygame.draw.rect(screen,color,rect.inflate(-8,-8),border_radius=3)
            if not building.built:
                pygame.draw.rect(screen,(70,54,31),(x-22,y+16,44,5))
                pygame.draw.rect(screen,(251,194,80),(x-22,y+16,int(44*building.progress/100),5))
            img=self.game.tiny_font.render(building.name,True,(245,240,205))
            screen.blit(img,(x-img.get_width()//2,y-37))
        for w in self.workers:
            if not w.alive:continue
            sx=int((w.x-camera_x)*CELL_SIZE)
            sy=int((w.y-camera_y)*CELL_SIZE)
            if not (-20<sx<sw+20 and -20<sy<sh+20):continue
            # Разные приборы заметны как инструменты на поясе.
            pygame.draw.circle(screen,(243,194,49),(sx,sy-8),6)
            pygame.draw.rect(screen,(237,129,29),(sx-6,sy-3,12,11),border_radius=2)
            pygame.draw.line(screen,(50,55,60),(sx-3,sy+8),(sx-5,sy+13),3)
            pygame.draw.line(screen,(50,55,60),(sx+3,sy+8),(sx+5,sy+13),3)
            device=(124,223,235) if 'Сканер' in w.tools else (95,240,156) if 'Строитель' in w.tools else (235,241,234)
            pygame.draw.line(screen,device,(sx+6,sy+2),(sx+13,sy-5),3)
            if w.vehicle is not None:
                pygame.draw.ellipse(screen,(40,50,60),(sx-15,sy-10,30,20),2)
            if w.hp<25:
                pygame.draw.rect(screen,(91,27,27),(sx-10,sy-21,20,4))
                pygame.draw.rect(screen,(75,227,104),(sx-10,sy-21,int(w.hp/25*20),4))
            name=self.game.tiny_font.render(str(w.number),True,(254,252,231))
            screen.blit(name,(sx-name.get_width()//2,sy-36))

    def panel(self):
        game=self.game;screen=game.screen
        pane=pygame.Surface((SCREEN_SIZE,SCREEN_SIZE),pygame.SRCALPHA)
        pane.fill((5,13,20,237));screen.blit(pane,(0,0))
        def txt(s,x,y,col=(227,235,231),big=False):
            font=game.font if big else game.small_font
            screen.blit(font.render(s,True,col),(x,y))
        txt('ПРОРАБ: 100 РАБОЧИХ — АВТОНОМНЫЙ РЕЖИМ',18,15,(255,208,94),True)
        alive=sum(w.alive for w in self.workers)
        txt(f'Живы: {alive} / 100 | Ходьба: {CREW_WALK_SPEED:.3f} клетки/кадр | Приказ: {self.order}',18,53)
        txt('Рабочие постоянно ходят, едят, лечатся, собирают и производят предметы.',18,78)
        txt('O / Esc закрыть | 1 авто  2 добыча  3 стройка  4 производство  5 охота',18,103,(173,220,242))
        pygame.draw.line(screen,(89,121,134),(18,136),(SCREEN_SIZE-18,136),1)
        txt('ПОСТРОЙКИ',20,153,(255,213,104),True)
        y=184
        for b in self.buildings:
            txt(b.name[:31]+(' [готово]' if b.built else f' [{b.progress:.0f}%]'),22,y)
            y+=22
        y+=13
        txt('ЗАПАСЫ',20,y,(255,213,104),True);y+=27
        for title,building in [('Склад и конвейер',self.store)]:
            qty=sum(building.stock.values())
            txt(f'{title}: {qty} ед.',25,y);y+=20
        right=SCREEN_SIZE//2+10
        txt('ПРОИЗВОДСТВО',right,153,(255,213,104),True)
        y=187
        for name in ('всего изготовлено','выдано снаряжения','добыто','поймано',
                     'в производство','сырьё на стройке','построено',
                     'вылечено','нападений хищников','убито хищников','потери','исследований'):
            txt(f'{name.capitalize()}: {self.stats[name]}',right,y)
            y+=25
        txt('Нажми O, чтобы вернуться в игру',right,SCREEN_SIZE-40,(156,219,231))

_original_foreman_init=Game.__init__
def _foreman_init(self):
    _original_foreman_init(self)
    self.foreman=ForemanCrew(self)
    pygame.display.set_caption('МегоСубнатика2D — прораб и автономные рабочие')
Game.__init__=_foreman_init

_original_foreman_update=Game.update
def _foreman_update(self):
    _original_foreman_update(self)
    if self.state == 'playing' and not self.settings_open and self.setting_workers_enabled:
        self.foreman.update()
Game.update=_foreman_update

_original_foreman_hud=Game.draw_hud
def _foreman_hud(self):
    _original_foreman_hud(self)
    if not self.setting_workers_enabled:
        return  # Ни счётчика NPC, ни подсказки O при одиночной игре.
    alive=sum(w.alive for w in self.foreman.workers)
    text=self.small_font.render(f'ПРОРАБ | Рабочие: {alive}/100 | O — бригада | Сделано: {self.foreman.stats["всего изготовлено"]}',True,(251,208,108))
    bg=pygame.Rect(7,232,text.get_width()+10,22)
    pygame.draw.rect(self.screen,(22,36,44),bg,border_radius=3)
    self.screen.blit(text,(11,234))
Game.draw_hud=_foreman_hud
# <<< PROраб_САМОСТОЯТЕЛЬНАЯ_БРИГАДА_V2 <<<


# -*- coding: utf-8 -*-
# Appendable built-in module: vegetable relatives and hexagonal symbionts.
# No external graphics or asset files are required.

# >>> VEGETABLE_AND_NUT_FISH_V1 >>>
VEGETABLE_NUT_BONUS_HP = 10         # HP per attached fish-nut; maximum 2 per bolt.
VEGETABLE_NUTS_PER_BOLT = 2
VEGETABLE_NUT_DETECTION = 3.4      # Threat radius (in map cells).
VEGETABLE_NUT_SEARCH = 6.0         # Bolt-seeking radius (in map cells).


def _draw_asteroid_new_fish(kind, length, width):
    """Draw a high-detail top-down sprite, rescale only at the end.

    Coordinates remain fixed, avoiding allocations inside the frame loop.
    Images can be regenerated at higher resolution by the original zoom code.
    """
    s = pygame.Surface((360, 240), pygame.SRCALPHA)
    d = pygame.draw
    pi = math.pi
    if kind == 'cucumber_fish':
        # Tail, paired glassy fins, elongated emerald body and fine prickles.
        d.polygon(s, (28, 114, 70), [(68, 120), (10, 61), (37, 111),
                                    (11, 182), (75, 130)])
        d.polygon(s, (93, 210, 109), [(60, 119), (18, 82), (39, 118),
                                      (18, 156), (64, 128)])
        for sign in (-1, 1):
            fin = [(153, 120 + sign*19), (182, 120 + sign*76),
                   (221, 120 + sign*69), (219, 120 + sign*16)]
            d.polygon(s, (25, 115, 80), fin)
            d.polygon(s, (78, 184, 94), [(159,120+sign*24),
                                         (181,120+sign*66),
                                         (210,120+sign*55),
                                         (213,120+sign*16)])
            d.line(s, (158, 232, 148), (182,120+sign*52), (211,120+sign*22), 3)
        outline = [(53,120),(72,102),(105,91),(157,89),(209,91),
                   (271,99),(307,111),(323,120),(305,129),
                   (270,140),(209,149),(155,151),(105,149),(72,138)]
        d.polygon(s,(10,75,53),outline)
        d.polygon(s,(51,174,80),[(64,120),(95,105),(157,99),(211,102),
                                 (268,110),(315,120),(271,132),(205,140),
                                 (153,142),(92,135)])
        d.polygon(s,(112,217,102),[(89,110),(151,104),(213,108),
                                   (269,115),(288,119),(212,120),(139,119)])
        d.lines(s,(183,248,154),False,[(85,108),(143,103),(206,107),(270,115)],4)
        # Rows of short spines; all are part of the animal, not projectiles.
        for i in range(15):
            x = 93+i*13
            h = max(5, int(9*math.sin(pi*(i+1)/16)))
            ytop = 91 + int(13*abs((x-180)/170))
            ybottom = 149 - int(11*abs((x-180)/170))
            for sign in (-1,1):
                y = ytop if sign < 0 else ybottom
                d.polygon(s,(25,125,50),[(x-4,y+sign*-1),
                                         (x,y+sign*h),(x+4,y+sign*-1)])
                d.line(s,(204,236,136),(x-1,y),(x,y+sign*h),1)
            if i % 2 == 0:
                d.circle(s,(29,117,58),(x+2,124),3)
                d.circle(s,(187,235,130),(x+1,123),1)
        d.circle(s,(9,45,48),(293,116),10)
        d.circle(s,(233,247,199),(293,116),7)
        d.circle(s,(25,30,35),(296,116),4)
        d.circle(s,(255,255,240),(295,113),2)
        d.arc(s,(19,101,71),(297,121,22,12),0.3,2.8,2)
    elif kind == 'patisson_fish':
        # Flattened, scalloped squash disk with a rigid toothed fin margin.
        cx, cy, outer = 180, 120, 96
        outer_poly = []
        for i in range(60):
            a = i*2*pi/60
            # Alternating crenellations; not a smooth circular puffer fish.
            r = outer*(1.0 if i % 3 != 0 else 1.105)
            outer_poly.append((cx+math.cos(a)*r,cy+math.sin(a)*r))
        d.polygon(s,(30,105,58),outer_poly)
        d.polygon(s,(91,155,57),[(cx+math.cos(i*2*pi/90)*87,
                                 cy+math.sin(i*2*pi/90)*87) for i in range(90)])
        d.circle(s,(161,198,75),(cx,cy),77)
        d.circle(s,(202,219,118),(cx-6,cy-8),64)
        # Corrugation from center to each hard fin tooth.
        for i in range(20):
            a = i*2*pi/20
            x1 = cx+math.cos(a)*28
            y1 = cy+math.sin(a)*28
            x2 = cx+math.cos(a)*92
            y2 = cy+math.sin(a)*92
            d.line(s,(62,126,60),(x1,y1),(x2,y2),5)
            d.line(s,(229,241,143),(x1,y1),(x2,y2),2)
        for j in range(18):
            a = j*2*pi/18
            x = cx+math.cos(a)*50
            y = cy+math.sin(a)*50
            d.circle(s,(112,165,75),(round(x),round(y)),5)
        d.circle(s,(75,118,48),(cx,cy),29)
        d.circle(s,(196,223,113),(cx-4,cy-5),25)
        d.circle(s,(238,241,164),(cx-9,cy-13),10)
        # Face, on the direction-of-travel side.
        for y in (102,139):
            d.circle(s,(26,65,48),(226,y),10)
            d.circle(s,(244,246,221),(228,y-1),7)
            d.circle(s,(17,34,24),(231,y),4)
            d.circle(s,(255,255,255),(229,y-3),2)
        d.arc(s,(43,90,49),(233,115,18,13),0.3,2.8,3)
        d.aalines(s,(25,83,45),True,outer_poly)
    elif kind == 'nut_fish':
        # A living copper-nickel nut, with a genuinely transparent aperture.
        cx, cy = 180, 120
        outer = [(cx+105*math.cos(i*pi/3),cy+105*math.sin(i*pi/3)) for i in range(6)]
        inner = [(cx+91*math.cos(i*pi/3),cy+91*math.sin(i*pi/3)) for i in range(6)]
        d.polygon(s,(40,63,72),outer)
        d.polygon(s,(132,166,174),inner)
        d.circle(s,(229,219,160),(cx-8,cy-12),77)
        d.circle(s,(125,170,170),(cx,cy),75,8)
        # The living body has an opening that is visually hollow.
        d.circle(s,(0,0,0,0),(cx,cy),58)
        d.circle(s,(26,53,58),(cx,cy),60,10)
        d.circle(s,(244,230,179),(cx,cy),66,4)
        for i in range(6):
            a = i*pi/3
            x1,y1 = (cx+math.cos(a)*78,cy+math.sin(a)*78)
            x2,y2 = (cx+math.cos(a)*98,cy+math.sin(a)*98)
            d.line(s,(248,237,182),(x1,y1),(x2,y2),5)
        # Thin swimming fins and eyes, separate from the metal-looking ring.
        for sign in (-1,1):
            y = cy+sign*77
            d.polygon(s,(45,140,166),[(115,y),(84,cy+sign*105),
                                     (152,cy+sign*90)])
        d.circle(s,(24,39,49),(260,106),11)
        d.circle(s,(148,245,250),(263,105),7)
        d.circle(s,(12,31,39),(265,106),4)
        d.circle(s,(250,250,250),(264,103),2)
        d.aalines(s,(40,55,63),True,outer)
    elif kind == 'bolt_fish':
        # Distinct fish-bolt is needed as a host: no bolt existed in the file.
        d.polygon(s,(41,104,145),[(93,104),(134,44),(170,83),(188,109)])
        d.polygon(s,(35,91,132),[(92,135),(134,196),(171,158),(188,133)])
        d.polygon(s,(32,51,69),[(17,120),(42,85),(69,71),(88,77),
                                 (91,100),(294,100),(333,120),(294,140),
                                 (91,140),(88,163),(69,169),(42,155)])
        d.polygon(s,(158,185,185),[(18,120),(43,91),(72,86),(86,104),
                                    (289,104),(322,120),(289,136),
                                    (86,136),(72,154),(43,149)])
        d.polygon(s,(212,228,222),[(92,107),(285,107),(307,116),
                                    (278,116),(98,116)])
        # Hexagonal head at the tail, threaded shaft, rounded pointed nose.
        hexagon = [(65+37*math.cos(i*pi/3),120+37*math.sin(i*pi/3)) for i in range(6)]
        d.polygon(s,(45,62,71),hexagon)
        d.polygon(s,(171,184,190),[(65+31*math.cos(i*pi/3),
                                    120+31*math.sin(i*pi/3)) for i in range(6)])
        d.circle(s,(64,100,117),(65,120),12)
        for x in range(112,283,13):
            d.line(s,(50,88,105),(x,103),(x+16,137),5)
            d.line(s,(247,248,230),(x+4,103),(x+20,136),2)
        d.circle(s,(10,35,43),(290,114),10)
        d.circle(s,(145,243,246),(291,113),6)
        d.circle(s,(22,35,44),(294,113),4)
        d.circle(s,(255,255,255),(292,110),2)
    else:
        raise ValueError('Unknown new fish sprite: '+str(kind))
    if kind in ('nut_fish','patisson_fish'):
        diameter = max(10,int(round(max(length,width)*1.25)))
        out_size = (diameter,diameter)
    else:
        out_size = (max(16,int(length*1.24)),max(13,int(width*1.85)))
    # No convert_alpha() here: it must also work while rendering preview surfaces.
    return pygame.transform.smoothscale(s,out_size)


_previous_creature_sprite_fn = generate_subnautica_creature_sprite


def generate_subnautica_creature_sprite(kind, length, width):
    if kind in ('cucumber_fish','patisson_fish','nut_fish','bolt_fish'):
        return _draw_asteroid_new_fish(kind,length,width)
    return _previous_creature_sprite_fn(kind,length,width)


_previous_init_assets_for_fish = init_assets


def _init_assets_with_vegetable_nuts():
    _previous_init_assets_for_fish()
    global cucumber_fish_type, patisson_fish_type, nut_fish_type, bolt_fish_type
    cucumber_fish_type = AnimalType(
        'Рыба-огурец', (65,181,78), 10, speed=.165, behavior='prey',
        hp=6, edible=True, hunger_value=17, thirst_value=14,
        fish_length=24, fish_width=7, sprite_kind='cucumber_fish',
        schooling=True, fear_distance=3.5, zigzag_strength=.27,
        speed_variation=.24, flee_speed_boost=2.0)
    patisson_fish_type = AnimalType(
        'Рыба-патиссон', (165,201,87), 14, speed=.022, behavior='prey',
        hp=zucchini_fish_type.hp * 3, edible=True, hunger_value=26,
        thirst_value=5, fish_length=15, fish_width=15,
        sprite_kind='patisson_fish', schooling=False,
        fear_distance=1.25, zigzag_strength=.025,
        speed_variation=.05, flee_speed_boost=1.15)
    nut_fish_type = AnimalType(
        'Рыба-гайка', (165,191,197), 9, speed=.11, behavior='prey',
        hp=5, edible=False, fish_length=9, fish_width=9,
        sprite_kind='nut_fish', fear_distance=3.3,
        zigzag_strength=.14, speed_variation=.12)
    bolt_fish_type = AnimalType(
        'Рыба-болт', (160,190,201), 15, speed=.085, behavior='neutral',
        hp=14, edible=False, fish_length=42, fish_width=10,
        sprite_kind='bolt_fish', fear_distance=1.5,
        zigzag_strength=.09, speed_variation=.14)
    # Physically correct ratio with the current game's existing zucchini HP.
    assert patisson_fish_type.hp == zucchini_fish_type.hp * 3
    densities = {
        'Реки':                ((cucumber_fish_type,5),(patisson_fish_type,2),
                               (bolt_fish_type,2),(nut_fish_type,2)),
        'Притоки':             ((cucumber_fish_type,3),(bolt_fish_type,1)),
        'Мелководье':          ((cucumber_fish_type,4),(patisson_fish_type,3),
                               (bolt_fish_type,3),(nut_fish_type,2)),
        'Озёра (внешние)':     ((cucumber_fish_type,5),(patisson_fish_type,3),
                               (bolt_fish_type,3),(nut_fish_type,2)),
        'Коралловый риф':      ((patisson_fish_type,3),(bolt_fish_type,2),
                               (nut_fish_type,3)),
        'Кельповый лес':       ((patisson_fish_type,2),(bolt_fish_type,2),
                               (nut_fish_type,2)),
        'Глубоководье':        ((bolt_fish_type,1),(nut_fish_type,1)),
    }
    for biome in biomes:
        if is_land_biome(biome) or getattr(biome,'aurora_style',False):
            continue
        biome.animals.extend(densities.get(biome.name,()))
    registry = globals().get('START_ITEM_REGISTRY')
    if isinstance(registry,dict):
        for creature in (cucumber_fish_type,patisson_fish_type,nut_fish_type,bolt_fish_type):
            registry[creature.name] = creature
    # This is also available to mods, scanner and custom tunnel population.
    globals()['VEGETABLE_NUT_FISH_TYPES'] = {
        'cucumber_fish':cucumber_fish_type,
        'patisson_fish':patisson_fish_type,
        'nut_fish':nut_fish_type,
        'bolt_fish':bolt_fish_type,
    }


init_assets = _init_assets_with_vegetable_nuts


# Fastener fish already wrap the HP property. Preserve their protections.
_old_asteroid_hp_prop = Animal.hp


def _vegetable_get_hp(self):
    return _old_asteroid_hp_prop.fget(self)


def _vegetable_set_hp(self, value):
    if (getattr(getattr(self,'type',None),'sprite_kind',None) == 'nut_fish'
            and getattr(self,'nut_bolt_host',None) is not None
            and value < _old_asteroid_hp_prop.fget(self)):
        return  # Attached nuts cannot be killed or captured separately.
    _old_asteroid_hp_prop.fset(self,value)


Animal.hp = property(_vegetable_get_hp,_vegetable_set_hp)
_old_nut_animal_update = Animal.update


def _nut_animal_update(self, player_x, player_y, world):
    if getattr(self,'nut_bolt_host',None) is not None:
        self.active = abs(self.x-player_x) <= VIEW_RADIUS+3 and abs(self.y-player_y) <= VIEW_RADIUS+3
        self.dx = self.dy = 0.0
        return
    return _old_nut_animal_update(self,player_x,player_y,world)


Animal.update = _nut_animal_update


def _nut_release(nut, give_bonus_back=True):
    bolt = getattr(nut,'nut_bolt_host',None)
    nut.nut_bolt_host = None
    if bolt is None:
        return
    passengers = getattr(bolt,'bolt_nuts',None)
    if passengers is not None and nut in passengers:
        passengers.remove(nut)
        if give_bonus_back:
            bolt.max_hp = max(bolt.type.hp, bolt.max_hp - VEGETABLE_NUT_BONUS_HP)
            # Preserve damage already taken under the temporary HP bonus.
            if bolt.hp > 0:
                bolt.hp = min(bolt.hp,bolt.max_hp)
    nut.nut_next_attach = 0
    nut._update_rotation()


def _nut_attach(game,nut,bolt):
    if getattr(nut,'nut_bolt_host',None) is not None:
        return False
    if getattr(bolt,'is_companion',False) or bolt.hp <= 0:
        return False
    passengers = getattr(bolt,'bolt_nuts',None)
    if passengers is None:
        passengers = []
        bolt.bolt_nuts = passengers
    if len(passengers) >= VEGETABLE_NUTS_PER_BOLT:
        return False
    nut.nut_bolt_host = bolt
    passengers.append(nut)
    bolt.max_hp += VEGETABLE_NUT_BONUS_HP
    bolt.hp += VEGETABLE_NUT_BONUS_HP
    game._nut_attached.add(nut)
    game._nut_seeking.pop(nut,None)
    nut.nut_next_attach = 0
    return True


def _nut_move_towards_bolt(game,nut,bolt):
    dx,dy = bolt.x-nut.x,bolt.y-nut.y
    dist = math.hypot(dx,dy)
    if dist < .75:
        _nut_attach(game,nut,bolt)
        return
    if dist <= 0.01:
        return
    step = min(.22,dist-.65)
    nx,ny = nut.x+dx/dist*step,nut.y+dy/dist*step
    world = game.cave_world if game.in_cave else game.world
    if not nut._can_swim_to(nx,ny,world):
        return
    oldx,oldy = nut.x,nut.y
    nut.x,nut.y = nx,ny
    nut.swim_heading = math.atan2(dy,dx)
    nut.cruise_heading = nut.swim_heading
    nut.dx,nut.dy = dx/dist*step,dy/dist*step
    nut._update_rotation()
    if not game.in_cave:
        game.world.update_animal_index(nut,oldx,oldy)


def _nut_tick(game):
    if getattr(game,'state',None) != 'playing' or getattr(game,'settings_open',False):
        return
    # A small per-game set, not a scan through every fish in a giant world.
    for nut in tuple(game._nut_attached):
        bolt = getattr(nut,'nut_bolt_host',None)
        # Bonds persist when the player changes between caves and the surface.
        current_layer = game.cave_animals if game.in_cave else game.world.animals
        other_layer = game.world.animals if game.in_cave else game.cave_animals
        if nut in other_layer and bolt in other_layer:
            continue  # Frozen but still attached off-screen.
        if (bolt is None or bolt.hp <= 0
                or getattr(bolt,'is_companion',False)
                or nut not in current_layer or bolt not in current_layer):
            _nut_release(nut)
            game._nut_attached.discard(nut)
            continue
        passenger_number = (getattr(bolt,'bolt_nuts',[]) or [nut]).index(nut)
        # Geometry: a ring clamped onto the threaded part of the fish-bolt.
        dx,dy = (math.cos(bolt.swim_heading),math.sin(bolt.swim_heading))
        lateralx,lateraly = -dy,dx
        nx = bolt.x - dx*(.13+.17*passenger_number) + lateralx*.10
        ny = bolt.y - dy*(.13+.17*passenger_number) + lateraly*.10
        oldx,oldy = nut.x,nut.y
        nut.x,nut.y = nx,ny
        nut.swim_heading = bolt.swim_heading
        nut.visual_angle = getattr(bolt,'visual_angle',0.0)
        nut.rotated_image = nut.type.get_rotated(nut.visual_angle)
        nut.rotated_glow = nut.type.get_rotated_glow(nut.visual_angle)
        if not game.in_cave:
            game.world.update_animal_index(nut,oldx,oldy)
    # Already alerted nuts smoothly swim toward their selected hosts every
    # frame; expensive target selection is only repeated every 10 frames.
    for nut,bolt in tuple(game._nut_seeking.items()):
        if (nut.hp <= 0 or bolt.hp <= 0
                or getattr(nut,'nut_bolt_host',None) is not None
                or getattr(bolt,'is_companion',False)
                or (game.in_cave and (nut not in game.cave_animals or bolt not in game.cave_animals))
                or (not game.in_cave and (nut not in game.world.animals or bolt not in game.world.animals))
                or (nut.x-game.player_x)**2+(nut.y-game.player_y)**2 > (VIEW_RADIUS+3)**2):
            game._nut_seeking.pop(nut,None)
            continue
        _nut_move_towards_bolt(game,nut,bolt)
    if game.frame_count % 10:
        return
    nearby = [a for a in game.get_nearby_animals(VIEW_RADIUS+2)
              if a.hp > 0 and not getattr(a,'is_companion',False)]
    bolts = [a for a in nearby if a.type is bolt_fish_type
             and len(getattr(a,'bolt_nuts',[])) < VEGETABLE_NUTS_PER_BOLT]
    if not bolts:
        return
    predators = [a for a in nearby if a.type.behavior == 'predator']
    for nut in nearby:
        if nut.type is not nut_fish_type or getattr(nut,'nut_bolt_host',None) is not None:
            continue
        if game.frame_count < getattr(nut,'nut_next_attach',0):
            continue
        threatened = ((nut.x-game.player_x)**2 + (nut.y-game.player_y)**2
                      <= VEGETABLE_NUT_DETECTION**2)
        if not threatened:
            threatened = any((p.x-nut.x)**2+(p.y-nut.y)**2<9
                             for p in predators if p is not nut)
        if not threatened:
            continue
        candidates = [bolt for bolt in bolts
                      if bolt.home_biome is nut.home_biome
                      and (bolt.x-nut.x)**2+(bolt.y-nut.y)**2 <= VEGETABLE_NUT_SEARCH**2]
        if not candidates:
            continue
        host = min(candidates,key=lambda a:(a.x-nut.x)**2+(a.y-nut.y)**2)
        game._nut_seeking[nut] = host
        _nut_move_towards_bolt(game,nut,host)


_old_nut_game_init = Game.__init__


def _vegetable_nut_game_init(self,*args,**kwargs):
    _old_nut_game_init(self,*args,**kwargs)
    self._nut_attached = set()
    self._nut_seeking = {}
    # Nuts grow in the immediate vicinity of their symbiotic bolt fish.
    # Seed some near natural hosts so players actually see this behaviour.
    rng = random.Random(20261031)
    bolts = [a for a in self.world.animals if a.type is bolt_fish_type]
    rng.shuffle(bolts)
    added = 0
    for bolt in bolts:
        if added >= 48:
            break
        if rng.random() > .36:
            continue
        a = rng.uniform(0,math.tau)
        x,y = bolt.x + math.cos(a)*.85,bolt.y + math.sin(a)*.85
        if (self.world.get_biome_at(x,y) is not bolt.home_biome
                or not self.world.is_water_at(x,y)
                or self.world.get_object_at(x,y) is not None):
            continue
        nut = Animal(x,y,nut_fish_type,bolt.home_biome)
        self.world.animals.append(nut)
        added += 1
    if added:
        self.world.rebuild_animal_index()


Game.__init__ = _vegetable_nut_game_init
_old_nut_game_update = Game.update


def _vegetable_nut_game_update(self,*args,**kwargs):
    _old_nut_game_update(self,*args,**kwargs)
    if hasattr(self,'_nut_attached'):
        _nut_tick(self)


Game.update = _vegetable_nut_game_update
_old_nut_game_remove = Game.remove_current_animal


def _vegetable_nut_remove(self,animal):
    passengers = list(getattr(animal,'bolt_nuts',()))
    self._nut_seeking.pop(animal,None)
    for prospect,host in tuple(self._nut_seeking.items()):
        if host is animal:
            self._nut_seeking.pop(prospect,None)
    if getattr(animal,'nut_bolt_host',None) is not None:
        _nut_release(animal)
        self._nut_attached.discard(animal)
    # Catching/removing a carrier frees its nuts. If it was caught, its
    # attached nuts enter the player's inventory, subject to free slots.
    for nut in passengers:
        _nut_release(nut)
        self._nut_attached.discard(nut)
        if nut in self.get_current_animals():
            if self.add_to_inventory(nut.type,wild_catch=True):
                _old_nut_game_remove(self,nut)
            else:
                nut.nut_next_attach = self.frame_count + 60
    return _old_nut_game_remove(self,animal)


Game.remove_current_animal = _vegetable_nut_remove
_old_nut_pick_up = Game.pick_up


def _vegetable_nut_pick_up(self,*args,**kwargs):
    if getattr(self,'state',None) != 'playing':
        return _old_nut_pick_up(self,*args,**kwargs)
    px,py = int(self.player_x),int(self.player_y)
    near = [a for a in self.get_nearby_animals(1.2) if a.hp > 0 and
            int(a.x)==px and int(a.y)==py and not getattr(a,'is_companion',False)]
    carriers = [a for a in near if a.type is bolt_fish_type and getattr(a,'bolt_nuts',())]
    if carriers:
        bolt = carriers[0]
        need = 1 + len(getattr(bolt,'bolt_nuts',()))
        free = sum(1 for item in self.inventory if item is None)
        if free < need:
            self.notification = f'Для болта с гайками требуется {need} свободных слотов'
            self.notification_timer = 90
            return
        if self.add_to_inventory(bolt.type,wild_catch=True):
            self.remove_current_animal(bolt)
            self.notification = f'Пойман болт со всеми гайками ({need} существ)'
            self.notification_timer = 95
        return
    nuts = [a for a in near if a.type is nut_fish_type and
            getattr(a,'nut_bolt_host',None) is not None]
    if nuts:
        self.notification = 'Рыба-гайка крепко держится за болт: ловите болт целиком!'
        self.notification_timer = 95
        return
    return _old_nut_pick_up(self,*args,**kwargs)


Game.pick_up = _vegetable_nut_pick_up
_old_nut_scan = Game._scanner_details


def _vegetable_nut_scan(self,target):
    result = _old_nut_scan(self,target)
    kind = getattr(target.get('entity'),'sprite_kind',None)
    extra = {
        'cucumber_fish':[
            'Рыба-огурец: близкий родственник рыбы-кабачка.',
            'Длина: 0,8 м. Вытянутое зелёное тело покрыто мелкими шипиками.',
            'Очень быстро плавает в реках, на мелководье и в озёрах.'],
        'patisson_fish':[
            'Рыба-патиссон: родственница рыб-кабачков и огурцов.',
            'Диаметр: 0,5 м. Плоское тело с зубчатым краем.',
            'Плавает у самого дна и защищается твёрдыми плавниками.',
            f'Тройная прочность: {patisson_fish_type.hp} HP вместо {zucchini_fish_type.hp}.'],
        'nut_fish':[
            'Рыба-гайка: живая гайка диаметром 0,3 м.',
            'При опасности прикрепляется к рыбе-болту.',
            'Поймать прикреплённую гайку отдельно нельзя: ловите болт.',
            f'Увеличивает запас HP носителя на {VEGETABLE_NUT_BONUS_HP}.'],
        'bolt_fish':[
            'Рыба-болт: носитель рыбы-гайки, дополнение к местной фауне.',
            f'Несёт до {VEGETABLE_NUTS_PER_BOLT} рыб-гаек.',
            'Каждая присосавшаяся гайка повышает текущий и максимум HP.',
            'При ловле болта все его гайки попадают в инвентарь.'],
    }
    if kind in extra:
        return list(result)+extra[kind]
    return result


Game._scanner_details = _vegetable_nut_scan
# <<< VEGETABLE_AND_NUT_FISH_V1 <<<

# >>> HYDROFISH_AND_LAND_PET_FIX_V1 >>>
HYDROFISH_NAME = 'Гидрорыба'
HYDROFISH_ATTACH_DURATION_MS = 2200
HYDROFISH_SMALL_PREY_HP = 18
HYDROFISH_BIG_PREY_HP = 24
HYDROFISH_LUNGE_RANGE = 1.15
AKLASUR_CAPTURE_HP = 35


def _entity_uses_land(entity_or_type):
    kind = getattr(entity_or_type, 'type', entity_or_type)
    return bool(getattr(kind, 'is_dungbird', False) or getattr(kind, 'is_aklasur', False))


def _entity_surface_passable(game, x, y, entity_or_type):
    if not (0 <= x < MAP_SIZE and 0 <= y < MAP_SIZE):
        return False
    if game.in_cave:
        return False
    cell_x = int(x)
    cell_y = int(y)
    target_object = game.world.get_object_at(cell_x, cell_y)
    if target_object is not None and target_object.is_wall:
        return False
    if game.is_start_capsule_wall(cell_x, cell_y):
        return False
    biome = game.world.get_biome_at(cell_x, cell_y)
    if biome is None or not biome.passable:
        return False
    kind = getattr(entity_or_type, 'type', entity_or_type)
    if getattr(kind, 'is_dungbird', False):
        # Птица может садиться и идти/лететь по любой проходимой поверхности.
        return True
    if getattr(kind, 'is_aklasur', False):
        return biome.name in LAND_BIOMES or biome.name in ('Мелководье', 'Реки', 'Притоки')
    if game.is_start_capsule_passage(cell_x, cell_y):
        return True
    return not is_land_biome(biome)


def _find_nearby_passable_position_for_entity(game, entity_or_type, radius=1.25):
    candidates = [(game.player_x, game.player_y)]
    for ring in (radius, radius * 1.6, radius * 2.1):
        for index in range(16):
            angle = index / 16.0 * math.tau
            candidates.append((game.player_x + math.cos(angle) * ring,
                               game.player_y + math.sin(angle) * ring))
    for x, y in candidates:
        if game.in_cave:
            if _entity_uses_land(entity_or_type):
                continue
            if game.cave_is_passable(x, y):
                return (x, y)
        else:
            if _entity_surface_passable(game, x, y, entity_or_type):
                return (x, y)
    return None


_old_spawn_from_inventory = Game._spawn_animal_from_inventory_item


def _spawn_animal_from_inventory_item_fixed(self, animal_type, companion=False):
    spawn = _find_nearby_passable_position_for_entity(self, animal_type, 1.25)
    if spawn is None:
        return None
    if self.in_cave:
        if _entity_uses_land(animal_type):
            return None
        home_biome = self.get_cave_tunnel_at(spawn[0], spawn[1])
    else:
        home_biome = self.world.get_biome_at(spawn[0], spawn[1])
        if home_biome is None:
            return None
        if not _entity_surface_passable(self, spawn[0], spawn[1], animal_type):
            return None
    if home_biome is None and (not companion):
        return None
    if home_biome is None:
        home_biome = (self.get_cave_tunnel_at(self.player_x, self.player_y)
                      if self.in_cave else self.world.get_biome_at(self.player_x, self.player_y))
    if home_biome is None:
        return None
    animal = Animal(spawn[0], spawn[1], animal_type, home_biome)
    if companion:
        animal.is_companion = True
        animal.hp = animal.max_hp
        animal.companion_orbit_angle = random.uniform(0.0, math.tau)
        animal.companion_orbit_radius = self._companion_orbit_distance(animal)
        animal.companion_target = None
        animal.companion_attack_timer = 0
        animal.companion_state = 'следует за игроком'
    self._add_animal_to_current_layer(animal)
    if getattr(animal.type, 'is_leech_hydra', False) and not hasattr(animal, 'hydra_tentacles'):
        _hydra_initialize_tentacles(animal)
    if getattr(animal.type, 'is_hydrofish', False):
        animal.hydrofish_attach_target = None
        animal.hydrofish_attach_until = 0
        animal.hydrofish_next_bite_at = 0
    return animal


Game._spawn_animal_from_inventory_item = _spawn_animal_from_inventory_item_fixed


def _position_is_passable_for_companion_fixed(self, x, y):
    # Обратная совместимость: для старых водных питомцев поведение не меняется.
    return _entity_surface_passable(self, x, y, getattr(self, '_companion_passability_probe', None) or getattr(self, 'selected_pet', None) or zucchini_fish_type)


Game._position_is_passable_for_companion = _position_is_passable_for_companion_fixed


def _move_companion_towards_fixed(self, animal, target_x, target_y, move_speed):
    to_target_x = target_x - animal.x
    to_target_y = target_y - animal.y
    distance = math.hypot(to_target_x, to_target_y)
    if distance <= 0.001:
        animal.dx = 0.0
        animal.dy = 0.0
        return False
    step = min(move_speed, distance)
    velocity_x = to_target_x / distance * step
    velocity_y = to_target_y / distance * step
    new_x = animal.x + velocity_x
    new_y = animal.y + velocity_y
    moved = False

    def passable(px, py):
        if self.in_cave:
            return (not _entity_uses_land(animal)) and self.cave_is_passable(px, py)
        return _entity_surface_passable(self, px, py, animal)

    if passable(new_x, new_y):
        animal.x = new_x
        animal.y = new_y
        moved = True
    elif passable(new_x, animal.y):
        animal.x = new_x
        velocity_y = 0.0
        moved = True
    elif passable(animal.x, new_y):
        animal.y = new_y
        velocity_x = 0.0
        moved = True
    else:
        animal.companion_orbit_angle += 0.35
    if moved:
        animal.dx = velocity_x
        animal.dy = velocity_y
        animal._update_rotation()
    else:
        animal.dx = 0.0
        animal.dy = 0.0
    return moved


Game._move_companion_towards = _move_companion_towards_fixed


def _transfer_visible_companions_fixed(self, companions, target_in_cave):
    if not companions:
        return
    player_on_land = (not target_in_cave) and self._is_player_on_land()
    changed_surface = False
    for index, animal in enumerate(companions):
        if target_in_cave and _entity_uses_land(animal):
            animal.companion_state = 'ждёт у входа в пещеру'
            continue
        if player_on_land and not _entity_uses_land(animal):
            animal.companion_state = 'ждёт у воды'
            continue
        self._clear_companion_target(animal)
        if target_in_cave:
            self.world.remove_animal(animal)
            if animal not in self.cave_animals:
                self.cave_animals.append(animal)
            animal.home_biome = self.get_cave_tunnel_at(self.player_x, self.player_y) or animal.home_biome
        else:
            if animal in self.cave_animals:
                self.cave_animals.remove(animal)
            if animal not in self.world.animals:
                self.world.animals.append(animal)
                changed_surface = True
            animal.home_biome = self.world.get_biome_at(self.player_x, self.player_y) or animal.home_biome
        angle = animal.companion_orbit_angle + index * 0.8
        radius = min(self._companion_follow_distance(animal), VIEW_RADIUS * 0.6)
        target_x = self.player_x + math.cos(angle) * radius
        target_y = self.player_y + math.sin(angle) * radius
        if ((target_in_cave and self.cave_is_passable(target_x, target_y)) or
                ((not target_in_cave) and _entity_surface_passable(self, target_x, target_y, animal))):
            animal.x = target_x
            animal.y = target_y
        else:
            animal.x = self.player_x
            animal.y = self.player_y
        animal.dx = 0.0
        animal.dy = 0.0
        animal.active = True
        animal.companion_state = 'следует за игроком'
    if not target_in_cave and changed_surface:
        self.world.rebuild_animal_index()


Game._transfer_visible_companions = _transfer_visible_companions_fixed


_old_pick_up_after_nuts = Game.pick_up


def _pick_up_with_live_aklasur(self, *args, **kwargs):
    if getattr(self, 'state', None) == 'playing' and not self.in_cave:
        px, py = int(self.player_x), int(self.player_y)
        for animal in self.get_nearby_animals(1.6):
            if (getattr(getattr(animal, 'type', None), 'is_aklasur', False)
                    and int(animal.x) == px and int(animal.y) == py and animal.hp > 0):
                if animal.is_companion or animal.hp <= AKLASUR_CAPTURE_HP:
                    if self.add_to_inventory(animal.type, wild_catch=not animal.is_companion):
                        self.remove_current_animal(animal)
                        self.notification = ('Акласур возвращён в инвентарь.'
                                             if animal.is_companion else
                                             'Ослабленный Акласур пойман живьём! H — сделать питомцем.')
                    else:
                        self.notification = 'Инвентарь заполнен — Акласура некуда поместить'
                    self.notification_timer = 90
                    return
                self.notification = f'Акласур слишком бодр. Ослабьте его хотя бы до {AKLASUR_CAPTURE_HP} HP.'
                self.notification_timer = 90
                return
    return _old_pick_up_after_nuts(self, *args, **kwargs)


Game.pick_up = _pick_up_with_live_aklasur


def generate_hydrofish_sprite(size=54):
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    k = size / 90.0
    def p(x, y):
        return (round(x * k), round(y * k))
    def ellipse(color, rect):
        x, y, w, h = rect
        pygame.draw.ellipse(surf, color, pygame.Rect(round(x*k), round(y*k), max(1, round(w*k)), max(1, round(h*k))))
    def poly(color, points):
        pygame.draw.polygon(surf, color, [p(x, y) for x, y in points])

    poly((49, 108, 122), [(12,45),(23,31),(44,25),(60,26),(75,35),(80,45),(75,55),(60,64),(42,65),(24,59)])
    poly((96, 176, 179), [(17,45),(28,34),(46,30),(60,31),(72,37),(75,45),(71,53),(58,59),(43,60),(27,55)])
    poly((175, 227, 214), [(22,42),(39,35),(58,36),(66,40),(68,45),(59,51),(42,54),(28,50)])
    poly((62, 94, 118), [(7,45),(0,35),(0,55)])
    poly((84, 130, 151), [(6,45),(1,38),(1,52)])
    poly((50, 130, 142), [(33,29),(41,15),(52,23),(46,33)])
    poly((36, 104, 118), [(32,60),(43,72),(55,64),(47,57)])
    ellipse((234, 241, 218), (56, 37, 14, 16))
    pygame.draw.circle(surf, (20, 29, 34), p(66, 44), max(1, round(3.2*k)))
    pygame.draw.circle(surf, (255, 255, 255), p(67, 43), max(1, round(1.2*k)))
    # Небольшие щупальца возле рта.
    for idx, base_y in enumerate((39, 42, 46, 49)):
        points = [p(69, base_y), p(78 + idx, base_y - 4 + idx % 2), p(86, base_y - 8 + idx)]
        pygame.draw.lines(surf, (205, 224, 214), False, points, max(1, round(2.3*k)))
        pygame.draw.circle(surf, (236, 244, 228), points[-1], max(1, round(1.5*k)))
    for x, y in ((29,39),(38,45),(30,52),(47,39),(53,47)):
        pygame.draw.arc(surf, (186, 223, 217), pygame.Rect(round((x-4)*k), round((y-4)*k), max(2, round(9*k)), max(2, round(9*k))), -0.9, 1.25, max(1, round(1.1*k)))
    return surf


def generate_big_patisson_sprite(size=84):
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    c = size // 2
    r = max(20, int(size * 0.33))
    points = []
    teeth = 14
    for i in range(teeth * 2):
        ang = i * math.pi / teeth
        radius = r * (1.18 if i % 2 == 0 else 0.90)
        points.append((c + math.cos(ang) * radius, c + math.sin(ang) * radius))
    pygame.draw.polygon(surf, (138, 184, 86), points)
    inner = []
    for x, y in points:
        inner.append((c + (x - c) * 0.86, c + (y - c) * 0.86))
    pygame.draw.polygon(surf, (184, 222, 105), inner)
    pygame.draw.circle(surf, (214, 236, 152), (c, c), int(r * 0.64))
    pygame.draw.circle(surf, (97, 132, 66), (c, c), int(r * 0.72), max(1, size // 28))
    for ang in [i * math.tau / 8 for i in range(8)]:
        x1 = c + math.cos(ang) * r * 0.18
        y1 = c + math.sin(ang) * r * 0.18
        x2 = c + math.cos(ang) * r * 0.78
        y2 = c + math.sin(ang) * r * 0.78
        pygame.draw.line(surf, (118, 169, 81), (round(x1), round(y1)), (round(x2), round(y2)), max(1, size // 34))
    # Жёсткие плавники-лепестки
    for ang in [i * math.tau / 4 + 0.35 for i in range(4)]:
        base1 = (c + math.cos(ang - 0.22) * r * 0.86, c + math.sin(ang - 0.22) * r * 0.86)
        tip = (c + math.cos(ang) * r * 1.46, c + math.sin(ang) * r * 1.46)
        base2 = (c + math.cos(ang + 0.22) * r * 0.86, c + math.sin(ang + 0.22) * r * 0.86)
        pygame.draw.polygon(surf, (84, 119, 72), [base1, tip, base2])
        pygame.draw.polygon(surf, (166, 205, 103), [((base1[0]+tip[0])*0.5, (base1[1]+tip[1])*0.5), tip, ((base2[0]+tip[0])*0.5, (base2[1]+tip[1])*0.5)])
    # Глазки и рот
    for ex in (-int(r*0.24), int(r*0.24)):
        pygame.draw.circle(surf, (235, 243, 220), (c + ex, c - int(r * 0.13)), max(2, size // 18))
        pygame.draw.circle(surf, (28, 36, 24), (c + ex, c - int(r * 0.13)), max(1, size // 34))
    mouth_rect = pygame.Rect(c - int(r * 0.22), c + int(r * 0.02), int(r * 0.44), int(r * 0.18))
    pygame.draw.arc(surf, (69, 96, 44), mouth_rect, 0.2, math.pi - 0.2, max(1, size // 28))
    return surf


_old_sprite_fn_hydro = generate_subnautica_creature_sprite


def generate_subnautica_creature_sprite(kind, length, width):
    if kind == 'hydrofish':
        return generate_hydrofish_sprite(max(44, int(max(length, width) * 1.2)))
    if kind == 'patisson_fish':
        return generate_big_patisson_sprite(max(54, int(max(length, width) * 1.24)))
    return _old_sprite_fn_hydro(kind, length, width)


_old_init_assets_hydro = init_assets


def init_assets_with_hydrofish_and_patisson_fix():
    _old_init_assets_hydro()
    global hydrofish_type, patisson_fish_type
    if 'patisson_fish_type' in globals():
        patisson_fish_type.fish_length = 45
        patisson_fish_type.fish_width = 45
        patisson_fish_type.image = generate_subnautica_creature_sprite('patisson_fish', 45, 45)
        patisson_fish_type.glow_image = None
        if hasattr(patisson_fish_type, 'rotated_cache'):
            patisson_fish_type.rotated_cache = {}
        if hasattr(patisson_fish_type, 'rotated_glow_cache'):
            patisson_fish_type.rotated_glow_cache = {}
    hydrofish_type = AnimalType(
        HYDROFISH_NAME, (86, 163, 167), 20, speed=.052,
        behavior='neutral', hp=18, damage=2, edible=True,
        hunger_value=25, thirst_value=7, fish_length=28, fish_width=15,
        sprite_kind='hydrofish', fear_distance=0.8,
        zigzag_strength=.08, speed_variation=.05)
    hydrofish_type.is_hydrofish = True
    hydrofish_type.fish_style = True
    hydrofish_type.image = generate_subnautica_creature_sprite('hydrofish', 28, 15)
    hydrofish_type.glow_image = None
    if 'dungbird_type' in globals():
        dungbird_type.can_live_on_land = True
    if 'aklasur_type' in globals():
        aklasur_type.can_live_on_land = True
    densities = {
        'Мелководье': 2,
        'Реки': 3,
        'Притоки': 2,
        'Коралловый риф': 2,
        'Кельповый лес': 1,
        'Озёра (внешние)': 2,
        'Биолюминесцентные зоны': 1,
    }
    for biome in biomes:
        if is_land_biome(biome) or getattr(biome, 'aurora_style', False):
            continue
        extra = densities.get(biome.name, 0)
        if extra:
            biome.animals.append((hydrofish_type, extra))
    registry = globals().get('START_ITEM_REGISTRY')
    if isinstance(registry, dict):
        registry[hydrofish_type.name] = hydrofish_type


init_assets = init_assets_with_hydrofish_and_patisson_fix


_old_hydro_game_update = Game.update


def _hydrofish_current_layer(game):
    return game.cave_animals if game.in_cave else game.world.animals


def _hydrofish_is_prey_candidate(subject, target):
    if target is subject or target.hp <= 0:
        return False
    if getattr(target, 'is_companion', False):
        return False
    if getattr(getattr(target, 'type', None), 'is_hydrofish', False):
        return False
    if not _hydra_is_fish(target):
        return False
    if getattr(getattr(target, 'type', None), 'is_leech_hydra', False):
        return False
    return True


def update_with_hydrofish(self, *args, **kwargs):
    _old_hydro_game_update(self, *args, **kwargs)
    if getattr(self, 'state', None) != 'playing' or getattr(self, 'settings_open', False):
        return
    now = pygame.time.get_ticks()
    current = _hydrofish_current_layer(self)
    for fish in self.get_nearby_animals(VIEW_RADIUS + 3):
        if fish.hp <= 0 or not getattr(getattr(fish, 'type', None), 'is_hydrofish', False):
            continue
        if not hasattr(fish, 'hydrofish_attach_target'):
            fish.hydrofish_attach_target = None
            fish.hydrofish_attach_until = 0
            fish.hydrofish_next_bite_at = 0
        host = fish.hydrofish_attach_target
        if host is not None:
            valid = (host in current and host.hp > 0 and now < fish.hydrofish_attach_until
                     and not getattr(host, 'is_companion', False))
            if not valid:
                fish.hydrofish_attach_target = None
            else:
                heading = getattr(host, 'swim_heading', 0.0)
                oldx, oldy = fish.x, fish.y
                fish.x = host.x - math.cos(heading) * 0.22 + math.sin(heading) * 0.10
                fish.y = host.y - math.sin(heading) * 0.22 - math.cos(heading) * 0.10
                fish.dx = host.dx
                fish.dy = host.dy
                fish.swim_heading = heading
                fish._update_rotation()
                if not self.in_cave:
                    self.world.update_animal_index(fish, oldx, oldy)
                if now >= fish.hydrofish_next_bite_at:
                    host.hp = max(0, host.hp - 1)
                    fish.hydrofish_next_bite_at = now + 420
                    if host.hp <= 0 and host in self.get_current_animals():
                        self.remove_current_animal(host)
                        fish.hydrofish_attach_target = None
                continue

        nearby_targets = []
        for target in current:
            if not _hydrofish_is_prey_candidate(fish, target):
                continue
            dist = math.hypot(target.x - fish.x, target.y - fish.y)
            if dist <= 2.2:
                nearby_targets.append((dist, target))
        if not nearby_targets:
            continue
        nearby_targets.sort(key=lambda item: item[0])
        dist, target = nearby_targets[0]
        target_size = max(getattr(target.type, 'fish_length', 0), getattr(target.type, 'volume', 0) * 10)
        if target.hp <= HYDROFISH_SMALL_PREY_HP and dist <= HYDROFISH_LUNGE_RANGE:
            target.hp = 0
            if target in self.get_current_animals():
                self.remove_current_animal(target)
            fish.dx = math.cos(fish.swim_heading) * fish.type.speed
            fish.dy = math.sin(fish.swim_heading) * fish.type.speed
            continue
        if target.hp >= HYDROFISH_BIG_PREY_HP and target_size >= 18 and dist <= 1.0:
            fish.hydrofish_attach_target = target
            fish.hydrofish_attach_until = now + HYDROFISH_ATTACH_DURATION_MS
            fish.hydrofish_next_bite_at = now + 280
            continue
        # Медленное подплывание к интересной добыче.
        if dist <= 1.8 and not getattr(fish, 'is_companion', False):
            dx = target.x - fish.x
            dy = target.y - fish.y
            inv = 1.0 / max(0.001, math.hypot(dx, dy))
            step = min(fish.type.speed * 1.55, dist)
            nx = fish.x + dx * inv * step
            ny = fish.y + dy * inv * step
            if fish._can_swim_to(nx, ny, self.cave_world if self.in_cave else self.world):
                oldx, oldy = fish.x, fish.y
                fish.x, fish.y = nx, ny
                fish.dx = dx * inv * step
                fish.dy = dy * inv * step
                fish.swim_heading = math.atan2(dy, dx)
                fish._update_rotation()
                if not self.in_cave:
                    self.world.update_animal_index(fish, oldx, oldy)


Game.update = update_with_hydrofish


_old_scanner_details_hydro = Game._scanner_details


def scanner_details_with_hydrofish_and_big_patisson(self, target):
    entity = target.get('entity')
    if getattr(entity, 'sprite_kind', None) == 'patisson_fish':
        return [
            'Рыба-патиссон: увеличенная тяжёлая родственница рыбы-кабачка и рыбы-огурца.',
            'Новый размер: около 1,5 м в диаметре — втрое больше прежнего.',
            'Имеет плоское дискообразное тело с зубчатым краем и жёсткими плавниками.',
            'Плавает медленно над дном, но отличается очень высокой живучестью.',
            f'Прочность: {patisson_fish_type.hp} HP, то есть втрое больше рыбы-кабачка.'
        ]
    if entity is globals().get('hydrofish_type'):
        return [
            'Гидрорыба — переходная форма между пиявкой-гидрой и примитивными рыбами.',
            'Внешность: рыба с короткими ротовыми щупальцами.',
            'Поведение: плавает медленно, хватает щупальцами мелкую рыбу.',
            'Может ненадолго присасываться к крупной добыче и ослаблять её.',
            'Обитает главным образом на мелководье, в реках, притоках и у рифов.'
        ]
    return _old_scanner_details_hydro(self, target)


Game._scanner_details = scanner_details_with_hydrofish_and_big_patisson
# <<< HYDROFISH_AND_LAND_PET_FIX_V1 <<<


if __name__ == '__main__':
    Game().run()