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


import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import numpy as np
import math
import random
import sys

# --- МАТЕМАТИЧЕСКИЕ УТИЛИТЫ ---
def vec3(x, y, z):
    return np.array([float(x), float(y), float(z)], dtype=np.float32)

def normalize(v):
    norm = np.linalg.norm(v)
    return v / norm if norm > 1e-6 else v

def distance(v1, v2):
    return np.linalg.norm(v1 - v2)

# --- ГЕНЕРАТОР ГЕОМЕТРИИ (3D-МЕШИ) ---
def draw_box(size, color, offset=vec3(0, 0, 0)):
    sx, sy, sz = size[0] / 2.0, size[1] / 2.0, size[2] / 2.0
    ox, oy, oz = offset[0], offset[1], offset[2]
    
    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, [*color, 1.0])
    glBegin(GL_QUADS)
    
    # Передняя грань
    glNormal3f(0.0, 0.0, 1.0)
    glVertex3f(ox - sx, oy - sy, oz + sz)
    glVertex3f(ox + sx, oy - sy, oz + sz)
    glVertex3f(ox + sx, oy + sy, oz + sz)
    glVertex3f(ox - sx, oy + sy, oz + sz)

    # Задняя грань
    glNormal3f(0.0, 0.0, -1.0)
    glVertex3f(ox - sx, oy - sy, oz - sz)
    glVertex3f(ox - sx, oy + sy, oz - sz)
    glVertex3f(ox + sx, oy + sy, oz - sz)
    glVertex3f(ox + sx, oy - sy, oz - sz)

    # Верхняя грань
    glNormal3f(0.0, 1.0, 0.0)
    glVertex3f(ox - sx, oy + sy, oz - sz)
    glVertex3f(ox - sx, oy + sy, oz + sz)
    glVertex3f(ox + sx, oy + sy, oz + sz)
    glVertex3f(ox + sx, oy + sy, oz - sz)

    # Нижняя грань
    glNormal3f(0.0, -1.0, 0.0)
    glVertex3f(ox - sx, oy - sy, oz - sz)
    glVertex3f(ox + sx, oy - sy, oz - sz)
    glVertex3f(ox + sx, oy - sy, oz + sz)
    glVertex3f(ox - sx, oy - sy, oz + sz)

    # Правая грань
    glNormal3f(1.0, 0.0, 0.0)
    glVertex3f(ox + sx, oy - sy, oz - sz)
    glVertex3f(ox + sx, oy + sy, oz - sz)
    glVertex3f(ox + sx, oy + sy, oz + sz)
    glVertex3f(ox + sx, oy - sy, oz + sz)

    # Левая грань
    glNormal3f(-1.0, 0.0, 0.0)
    glVertex3f(ox - sx, oy - sy, oz - sz)
    glVertex3f(ox - sx, oy - sy, oz + sz)
    glVertex3f(ox - sx, oy + sy, oz + sz)
    glVertex3f(ox - sx, oy + sy, oz - sz)
    glEnd()

def draw_cylinder(radius, length, segments=12, color=(0.2, 0.2, 0.2)):
    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, [*color, 1.0])
    glBegin(GL_QUAD_STRIP)
    for i in range(segments + 1):
        angle = 2.0 * math.pi * i / segments
        x = radius * math.cos(angle)
        y = radius * math.sin(angle)
        glNormal3f(math.cos(angle), math.sin(angle), 0.0)
        glVertex3f(x, y, 0.0)
        glVertex3f(x, y, length)
    glEnd()

# --- СИСТЕМА ЧАСТИЦ (ВЗРЫВЫ, ДЫМ) ---
class ParticleSystem:
    def __init__(self):
        self.particles = []

    def emit(self, pos, count=20, color=(1.0, 0.5, 0.0), speed=8.0, size=0.25):
        for _ in range(count):
            vel = vec3(
                random.uniform(-1, 1),
                random.uniform(0.5, 2.0),
                random.uniform(-1, 1)
            )
            vel = normalize(vel) * random.uniform(speed * 0.4, speed)
            self.particles.append({
                'pos': np.copy(pos),
                'vel': vel,
                'life': 1.0,
                'decay': random.uniform(1.2, 2.5),
                'color': color,
                'size': size
            })

    def update(self, dt):
        for p in self.particles[:]:
            p['pos'] += p['vel'] * dt
            p['vel'][1] -= 9.8 * dt * 0.5  # гравитация
            p['life'] -= p['decay'] * dt
            if p['life'] <= 0:
                self.particles.remove(p)

    def draw(self):
        glDisable(GL_LIGHTING)
        for p in self.particles:
            c = p['color']
            alpha = max(0.0, p['life'])
            glColor4f(c[0], c[1], c[2], alpha)
            glPushMatrix()
            glTranslatef(p['pos'][0], p['pos'][1], p['pos'][2])
            s = p['size'] * p['life']
            draw_box((s, s, s), c)
            glPopMatrix()
        glEnable(GL_LIGHTING)

# --- СНАРЯД ---
class Shell:
    def __init__(self, pos, direction, is_player=True):
        self.pos = np.copy(pos)
        self.dir = normalize(direction)
        self.speed = 70.0
        self.is_player = is_player
        self.active = True
        self.life = 2.5

    def update(self, dt):
        self.pos += self.dir * self.speed * dt
        self.life -= dt
        if self.life <= 0:
            self.active = False

    def draw(self):
        glPushMatrix()
        glTranslatef(self.pos[0], self.pos[1], self.pos[2])
        col = (1.0, 0.9, 0.2) if self.is_player else (1.0, 0.2, 0.2)
        draw_box((0.25, 0.25, 0.6), col)
        glPopMatrix()

# --- СЛОЖНАЯ МОДЕЛЬ ТАНКА ---
class Tank:
    def __init__(self, x, z, base_color, is_player=False):
        self.pos = vec3(x, 0.0, z)
        self.rotation_y = 0.0       # угол корпуса
        self.turret_angle = 0.0     # локальный поворот башни
        self.recoil = 0.0           # откат ствола при выстреле
        self.color = base_color
        self.is_player = is_player
        
        self.max_hp = 5 if is_player else 3
        self.hp = self.max_hp
        self.speed = 12.0 if is_player else 7.0
        self.turn_speed = 90.0
        self.shoot_cd = 0.0
        self.alive = True

    def get_forward_vector(self):
        rad = math.radians(self.rotation_y)
        return vec3(math.sin(rad), 0.0, math.cos(rad))

    def get_turret_forward(self):
        rad = math.radians(self.rotation_y + self.turret_angle)
        return vec3(math.sin(rad), 0.0, math.cos(rad))

    def update(self, dt):
        if self.shoot_cd > 0:
            self.shoot_cd -= dt
        if self.recoil > 0:
            self.recoil = max(0.0, self.recoil - dt * 3.5)

    def draw(self):
        if not self.alive:
            return

        glPushMatrix()
        glTranslatef(self.pos[0], self.pos[1], self.pos[2])
        glRotatef(self.rotation_y, 0, 1, 0)

        # 1. Траки (левая и правая гусеницы)
        track_color = (0.15, 0.15, 0.15)
        draw_box((0.7, 0.6, 4.4), track_color, offset=vec3(-1.3, 0.3, 0.0))
        draw_box((0.7, 0.6, 4.4), track_color, offset=vec3(1.3, 0.3, 0.0))

        # Катки
        wheel_color = (0.28, 0.28, 0.3)
        for side in (-1.3, 1.3):
            for z_off in (-1.6, -0.8, 0.0, 0.8, 1.6):
                draw_box((0.72, 0.45, 0.45), wheel_color, offset=vec3(side, 0.25, z_off))

        # 2. Нижняя и верхняя броня корпуса
        draw_box((2.2, 0.6, 4.0), self.color, offset=vec3(0.0, 0.5, 0.0))
        draw_box((2.0, 0.3, 3.6), [c * 0.85 for c in self.color], offset=vec3(0.0, 0.8, -0.1))

        # 3. Башня
        glPushMatrix()
        glTranslatef(0.0, 1.05, 0.0)
        glRotatef(self.turret_angle, 0, 1, 0)

        # Купол башни
        draw_box((1.8, 0.65, 2.2), self.color, offset=vec3(0.0, 0.0, -0.2))
        # Маска орудия
        draw_box((0.8, 0.4, 0.6), (0.1, 0.1, 0.1), offset=vec3(0.0, 0.0, 0.9))
        # Командирская башенка
        draw_box((0.5, 0.25, 0.5), (0.2, 0.2, 0.2), offset=vec3(0.45, 0.4, -0.4))

        # Ствол с откатом
        glPushMatrix()
        glTranslatef(0.0, 0.0, 1.1 - self.recoil)
        draw_cylinder(0.12, 2.6, segments=12, color=(0.15, 0.15, 0.15))
        # Дульный тормоз
        draw_box((0.32, 0.32, 0.5), (0.1, 0.1, 0.1), offset=vec3(0.0, 0.0, 2.6))
        glPopMatrix()

        glPopMatrix() # Конец башни
        glPopMatrix() # Конец танка

        # Отрисовка полосы HP над танком
        self.draw_hp_bar()

    def draw_hp_bar(self):
        glDisable(GL_LIGHTING)
        glPushMatrix()
        glTranslatef(self.pos[0], self.pos[1] + 2.6, self.pos[2])
        
        # Разворот лицом к камере (билбординг)
        mat = glGetFloatv(GL_MODELVIEW_MATRIX)
        # Очищаем вращение для UI
        for i in range(3):
            for j in range(3):
                mat[i][j] = 1.0 if i == j else 0.0
        glLoadMatrixf(mat)

        # Фон HP
        glColor3f(0.1, 0.1, 0.1)
        draw_box((1.6, 0.16, 0.02), (0.1, 0.1, 0.1))

        # Текущее HP
        pct = max(0.0, self.hp / self.max_hp)
        fill_col = (0.2, 0.9, 0.2) if pct > 0.5 else ((0.9, 0.6, 0.1) if pct > 0.25 else (0.9, 0.1, 0.1))
        glColor3f(*fill_col)
        draw_box((1.56 * pct, 0.12, 0.04), fill_col, offset=vec3(-0.78 * (1.0 - pct), 0.0, 0.01))

        glPopMatrix()
        glEnable(GL_LIGHTING)

# --- РАЗРУШАЕМЫЕ ЗДАНИЯ И ПРЕПЯТСТВИЯ ---
class Building:
    def __init__(self, x, z, sx, sy, sz):
        self.pos = vec3(x, sy / 2.0, z)
        self.size = vec3(sx, sy, sz)
        self.hp = 3
        self.alive = True
        self.color = (0.45, 0.42, 0.4)

    def draw(self):
        if not self.alive:
            return
        draw_box(self.size, self.color, offset=self.pos)

    def get_aabb(self):
        hs = self.size / 2.0
        return self.pos - hs, self.pos + hs

# --- ОСНОВНОЙ КЛАСС ИГРЫ ---
class TankWarGame:
    def __init__(self):
        pygame.init()
        self.res = (1280, 720)
        pygame.display.set_mode(self.res, DOUBLEBUF | OPENGL)
        pygame.display.set_caption("3D Panzer Force")
        pygame.mouse.set_visible(False)
        pygame.event.set_grab(True)

        self.clock = pygame.time.Clock()
        self.particles = ParticleSystem()
        
        self.map_size = 140.0
        self.score = 0
        self.game_over = False

        self.init_gl()
        self.init_world()

    def init_gl(self):
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
        glEnable(GL_COLOR_MATERIAL)
        glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE)

        # Освещение
        glLightfv(GL_LIGHT0, GL_POSITION, [40.0, 60.0, 40.0, 1.0])
        glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.9, 0.88, 0.85, 1.0])
        glLightfv(GL_LIGHT0, GL_AMBIENT, [0.35, 0.35, 0.38, 1.0])
        glLightfv(GL_LIGHT0, GL_SPECULAR, [0.4, 0.4, 0.4, 1.0])

        glClearColor(0.53, 0.75, 0.92, 1.0) # Цвет неба

        # Проекция
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(65, (self.res[0] / self.res[1]), 0.5, 300.0)
        glMatrixMode(GL_MODELVIEW)

    def init_world(self):
        self.player = Tank(0, -30, (0.2, 0.55, 0.25), is_player=True)
        self.enemies = []
        self.bullets = []
        self.buildings = []

        # Генерация укрытий (городские руины)
        random.seed(42)
        for _ in range(30):
            bx = random.uniform(-self.map_size / 2 + 15, self.map_size / 2 - 15)
            bz = random.uniform(-self.map_size / 2 + 15, self.map_size / 2 - 15)
            if abs(bx) > 10 or abs(bz) > 10:
                self.buildings.append(Building(bx, bz, random.choice([5, 8]), random.choice([4, 6]), random.choice([5, 8])))

        # Враги
        spawn_coords = [(-35, 25), (0, 40), (35, 25), (-40, -10), (40, -10)]
        for ex, ez in spawn_coords:
            self.enemies.append(Tank(ex, ez, (0.7, 0.25, 0.2)))

        self.cam_yaw = 0.0
        self.cam_pitch = 18.0
        self.cam_dist = 14.0

    def handle_input(self, dt):
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    pygame.quit()
                    sys.exit()
                if event.key == K_r and self.game_over:
                    self.__init__()
                    return

            # Вращение башни и камеры мышью
            if event.type == MOUSEMOTION:
                dx, dy = event.rel
                self.cam_yaw -= dx * 0.2
                self.cam_pitch = max(8.0, min(45.0, self.cam_pitch + dy * 0.15))

            # Выстрел
            if event.type == MOUSEBUTTONDOWN and event.button == 1:
                if self.player.alive and self.player.shoot_cd <= 0:
                    self.fire_tank(self.player)

        if not self.player.alive:
            return

        keys = pygame.key.get_pressed()
        # Поворот корпуса танка
        if keys[K_a]:
            self.player.rotation_y += self.player.turn_speed * dt
        if keys[K_d]:
            self.player.rotation_y -= self.player.turn_speed * dt

        # Движение вперед / назад с проверкой коллизий
        move_dir = 0.0
        if keys[K_w]:
            move_dir += 1.0
        if keys[K_s]:
            move_dir -= 0.6

        if move_dir != 0:
            forward = self.player.get_forward_vector()
            new_pos = self.player.pos + forward * (move_dir * self.player.speed * dt)
            
            # Проверка границ карты
            limit = self.map_size / 2.0 - 3.0
            if abs(new_pos[0]) < limit and abs(new_pos[2]) < limit:
                # Проверка столкновений со зданиями
                blocked = False
                for b in self.buildings:
                    if b.alive and distance(new_pos, b.pos) < 4.5:
                        blocked = True
                        break
                if not blocked:
                    self.player.pos = new_pos

        # Башня стремится к направлению обзора камеры
        target_turret = (self.cam_yaw - self.player.rotation_y) % 360
        if target_turret > 180:
            target_turret -= 360
        self.player.turret_angle = target_turret

    def fire_tank(self, tank):
        tank.shoot_cd = 0.6 if tank.is_player else 2.5
        tank.recoil = 0.4
        fwd = tank.get_turret_forward()
        spawn_pos = tank.pos + vec3(0, 1.05, 0) + fwd * 3.6
        self.bullets.append(Shell(spawn_pos, fwd, is_player=tank.is_player))
        self.particles.emit(spawn_pos, count=12, color=(1.0, 0.7, 0.2), speed=6.0, size=0.2)

    def update_ai(self, dt):
        for e in self.enemies:
            if not e.alive:
                continue

            dist_to_p = distance(e.pos, self.player.pos)
            to_player = normalize(self.player.pos - e.pos)
            
            # Наведение башни на игрока
            target_yaw = math.degrees(math.atan2(to_player[0], to_player[2]))
            rel_yaw = (target_yaw - e.rotation_y) % 360
            if rel_yaw > 180:
                rel_yaw -= 360
            e.turret_angle = rel_yaw

            # Поворот корпуса к игроку и маневрирование
            e.rotation_y += np.sign(rel_yaw) * e.turn_speed * 0.4 * dt

            # Сближение при большой дистанции
            if dist_to_p > 16.0:
                fwd = e.get_forward_vector()
                e.pos += fwd * e.speed * dt

            # Огонь с таймером
            if dist_to_p < 55.0 and e.shoot_cd <= 0 and self.player.alive:
                self.fire_tank(e)

            e.update(dt)

    def update_physics(self, dt):
        self.player.update(dt)
        self.update_ai(dt)
        self.particles.update(dt)

        # Обновление снарядов
        for b in self.bullets[:]:
            b.update(dt)
            if not b.active:
                self.bullets.remove(b)
                continue

            # Коллизии со зданиями
            hit_building = False
            for bld in self.buildings:
                if bld.alive and distance(b.pos, bld.pos) < 3.8:
                    bld.hp -= 1
                    if bld.hp <= 0:
                        bld.alive = False
                        self.particles.emit(bld.pos, count=35, color=(0.4, 0.4, 0.4), speed=10.0, size=0.5)
                    self.particles.emit(b.pos, count=10, color=(0.6, 0.5, 0.3), speed=5.0)
                    b.active = False
                    hit_building = True
                    break
            if hit_building:
                self.bullets.remove(b)
                continue

            # Попадание по танкам
            if b.is_player:
                for e in self.enemies:
                    if e.alive and distance(b.pos, e.pos + vec3(0, 0.8, 0)) < 2.2:
                        e.hp -= 1
                        self.particles.emit(b.pos, count=25, color=(1.0, 0.3, 0.1), speed=12.0)
                        if e.hp <= 0:
                            e.alive = False
                            self.score += 100
                            self.particles.emit(e.pos + vec3(0, 1, 0), count=60, color=(0.9, 0.4, 0.1), speed=15.0, size=0.6)
                        b.active = False
                        break
            else:
                if self.player.alive and distance(b.pos, self.player.pos + vec3(0, 0.8, 0)) < 2.2:
                    self.player.hp -= 1
                    self.particles.emit(b.pos, count=30, color=(1.0, 0.2, 0.1), speed=14.0)
                    if self.player.hp <= 0:
                        self.player.alive = False
                        self.game_over = True
                        self.particles.emit(self.player.pos + vec3(0, 1, 0), count=80, color=(0.9, 0.3, 0.1), speed=16.0, size=0.8)
                    b.active = False

            if not b.active and b in self.bullets:
                self.bullets.remove(b)

        # Проверка победы
        if all(not e.alive for e in self.enemies):
            self.game_over = True

    def render_ground(self):
        # Шахматное текстурирование арены
        glDisable(GL_LIGHTING)
        step = 10
        half = int(self.map_size / 2)
        glBegin(GL_QUADS)
        for x in range(-half, half, step):
            for z in range(-half, half, step):
                if (x // step + z // step) % 2 == 0:
                    glColor3f(0.32, 0.45, 0.28)
                else:
                    glColor3f(0.35, 0.48, 0.30)
                glVertex3f(x, 0.0, z)
                glVertex3f(x + step, 0.0, z)
                glVertex3f(x + step, 0.0, z + step)
                glVertex3f(x, 0.0, z + step)
        glEnd()
        glEnable(GL_LIGHTING)

    def draw_hud(self):
        # Отрисовка 2D-интерфейса поверх 3D
        glMatrixMode(GL_PROJECTION)
        glPushMatrix()
        glLoadIdentity()
        gluOrtho2D(0, self.res[0], 0, self.res[1])
        glMatrixMode(GL_MODELVIEW)
        glPushMatrix()
        glLoadIdentity()
        glDisable(GL_LIGHTING)
        glDisable(GL_DEPTH_TEST)

        font = pygame.font.SysFont("impact", 26)
        
        # Индикатор очков и живых врагов
        alive_enemies = sum(1 for e in self.enemies if e.alive)
        stats_surf = font.render(f"ОЧКИ: {self.score} | ЦЕЛИ: {alive_enemies}", True, (255, 230, 80))
        stats_data = pygame.image.tostring(stats_surf, "RGBA", True)
        
        glRasterPos2i(30, self.res[1] - 45)
        glDrawPixels(stats_surf.get_width(), stats_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, stats_data)

        # Прицел по центру экрана
        cx, cy = self.res[0] // 2, self.res[1] // 2
        glColor3f(1.0, 1.0, 1.0)
        glBegin(GL_LINES)
        glVertex2f(cx - 12, cy); glVertex2f(cx + 12, cy)
        glVertex2f(cx, cy - 12); glVertex2f(cx, cy + 12)
        glEnd()

        if self.game_over:
            msg = "МИССИЯ ВЫПОЛНЕНА!" if self.player.alive else "МАШИНА УНИЧТОЖЕНА!"
            col = (50, 255, 50) if self.player.alive else (255, 50, 50)
            end_font = pygame.font.SysFont("impact", 48)
            end_surf = end_font.render(msg, True, col)
            end_data = pygame.image.tostring(end_surf, "RGBA", True)
            glRasterPos2i(self.res[0] // 2 - end_surf.get_width() // 2, self.res[1] // 2 + 20)
            glDrawPixels(end_surf.get_width(), end_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, end_data)

            sub_font = pygame.font.SysFont("consolas", 20, bold=True)
            sub_surf = sub_font.render("Нажмите R для рестарта или ESC для выхода", True, (255, 255, 255))
            sub_data = pygame.image.tostring(sub_surf, "RGBA", True)
            glRasterPos2i(self.res[0] // 2 - sub_surf.get_width() // 2, self.res[1] // 2 - 30)
            glDrawPixels(sub_surf.get_width(), sub_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, sub_data)

        glEnable(GL_DEPTH_TEST)
        glEnable(GL_LIGHTING)
        glPopMatrix()
        glMatrixMode(GL_PROJECTION)
        glPopMatrix()
        glMatrixMode(GL_MODELVIEW)

    def run(self):
        while True:
            dt = self.clock.tick(60) / 1000.0
            self.handle_input(dt)
            if not self.game_over:
                self.update_physics(dt)

            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
            glLoadIdentity()

            # Вычисление камеры от 3-го лица
            rad_yaw = math.radians(self.cam_yaw)
            rad_pitch = math.radians(self.cam_pitch)
            
            cam_x = self.player.pos[0] - self.cam_dist * math.sin(rad_yaw) * math.cos(rad_pitch)
            cam_y = self.player.pos[1] + self.cam_dist * math.sin(rad_pitch) + 1.5
            cam_z = self.player.pos[2] - self.cam_dist * math.cos(rad_yaw) * math.cos(rad_pitch)

            target = self.player.pos + vec3(0, 1.2, 0)
            gluLookAt(cam_x, cam_y, cam_z, target[0], target[1], target[2], 0, 1, 0)

            # Отрисовка мира
            self.render_ground()
            for bld in self.buildings:
                bld.draw()
            self.player.draw()
            for e in self.enemies:
                e.draw()
            for b in self.bullets:
                b.draw()
            self.particles.draw()

            self.draw_hud()
            pygame.display.flip()

if __name__ == "__main__":
    game = TankWarGame()
    game.run()