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


from ursina import *
import random

app = Ursina()

# --- ОКРУЖЕНИЕ И НАСТРОЙКА ЭКРАНА ---
window.title = "3D Battle Tanks"
window.borderless = False
window.fullscreen = False
window.fps_counter.enabled = True

# Небо и мягкое освещение
Sky(color=color.rgb(135, 185, 235))
DirectionalLight(y=20, rotation=(45, -30, 20), shadows=True)
AmbientLight(color=color.rgb(100, 105, 115))

MAP_RADIUS = 70

# Контрастная земля с сеткой для ощущения масштаба
ground = Entity(
    model='plane',
    scale=(MAP_RADIUS * 2, 1, MAP_RADIUS * 2),
    color=color.rgb(55, 75, 45),
    texture='white_cube',
    texture_scale=(MAP_RADIUS, MAP_RADIUS),
    collider='box'
)

# Границы карты
for angle in range(0, 360, 45):
    rad = math.radians(angle)
    x = math.cos(rad) * MAP_RADIUS
    z = math.sin(rad) * MAP_RADIUS
    Entity(model='cube', position=(x, 2, z), scale=(15, 5, 2), rotation_y=-angle,
           color=color.rgb(100, 40, 40), collider='box')

# Разрушаемые бункеры и кирпичные стены
destructibles = []
for _ in range(25):
    rx = random.uniform(-MAP_RADIUS + 15, MAP_RADIUS - 15)
    rz = random.uniform(-MAP_RADIUS + 15, MAP_RADIUS - 15)
    if abs(rx) > 10 or abs(rz) > 10:
        bunker = Entity(
            model='cube',
            position=(rx, 1.5, rz),
            scale=(random.choice([4, 6]), 3, random.choice([4, 6])),
            color=color.rgb(130, 95, 65),
            texture='white_cube',
            collider='box'
        )
        bunker.hp = 3
        destructibles.append(bunker)

# --- СПЕЦЭФФЕКТЫ ---
def spawn_fx(pos, scale=2.0, tint=color.orange):
    p = Entity(model='sphere', position=pos, scale=0.3, color=tint)
    p.animate_scale(scale, duration=0.2, curve=curve.out_expo)
    p.animate_color(color.clear, duration=0.2)
    destroy(p, delay=0.25)

# --- СНАРЯД ---
class Shell(Entity):
    def __init__(self, pos, direction, is_player=True):
        super().__init__(
            model='sphere',
            scale=0.35,
            color=color.yellow if is_player else color.red,
            position=pos,
            collider='sphere'
        )
        self.direction = direction.normalized()
        self.is_player = is_player
        self.speed = 65
        self.life = 2.5

    def update(self):
        self.position += self.direction * self.speed * time.dt
        self.life -= time.dt
        if self.life <= 0:
            destroy(self)
            return

        hit = self.intersects()
        if hit.hit and hit.entity != ground:
            ent = hit.entity
            spawn_fx(self.position, scale=2.5, tint=color.orange)

            # Урон постройкам
            if ent in destructibles:
                ent.hp -= 1
                ent.color = color.rgb(180, 70, 50)
                if ent.hp <= 0:
                    destructibles.remove(ent)
                    destroy(ent)
                destroy(self)
                return

            # Урон врагам
            if self.is_player and hasattr(ent, 'tank_ref') and isinstance(ent.tank_ref, EnemyTank):
                ent.tank_ref.take_damage(1)
                destroy(self)
                return

            # Урон игроку
            if not self.is_player and hasattr(ent, 'tank_ref') and ent.tank_ref == player:
                player.take_damage(1)
                destroy(self)
                return

            destroy(self)

# --- БАЗОВЫЙ СОСТАВНОЙ ТАНК ---
class DetailedTank(Entity):
    def __init__(self, pos, camo_color):
        super().__init__(position=pos)
        self.max_hp = 3
        self.hp = 3

        # Корпус
        self.hull = Entity(parent=self, model='cube', scale=(3.2, 0.75, 4.8), y=0.6,
                           color=camo_color, texture='white_cube')
        self.hull.collider = 'box'
        self.hull.tank_ref = self

        # Левая и правая гусеницы с фальшбортами
        for side in (-1.7, 1.7):
            Entity(parent=self, model='cube', scale=(0.7, 0.9, 5.2), position=(side, 0.45, 0),
                   color=color.rgb(30, 30, 30))
            # Катки
            for z in (-1.8, -0.9, 0, 0.9, 1.8):
                Entity(parent=self, model='cylinder', scale=(0.4, 0.72, 0.4), rotation_z=90,
                       position=(side, 0.4, z), color=color.rgb(70, 70, 70))

        # Башня
        self.turret = Entity(parent=self, model='cube', scale=(2.2, 0.65, 2.6), y=1.2,
                             color=camo_color, texture='white_cube')
        self.turret.collider = 'box'
        self.turret.tank_ref = self

        # Маска орудия + командирская башенка
        Entity(parent=self.turret, model='cube', scale=(0.8, 0.4, 0.5), position=(0, 0.05, 1.3), color=color.black)
        Entity(parent=self.turret, model='cylinder', scale=(0.6, 0.3, 0.6), position=(0.5, 0.4, -0.4), color=color.dark_gray)

        # Ствол орудия и дульный тормоз
        self.barrel = Entity(parent=self.turret, model='cylinder', scale=(0.22, 3.2, 0.22),
                             rotation_x=90, position=(0, 0.05, 2.8), color=color.rgb(20, 20, 20))
        Entity(parent=self.barrel, model='cube', scale=(1.8, 0.25, 1.5), position=(0, 1.55, 0), color=color.black)

        # Индикатор здоровья (над башней)
        self.hp_bar_bg = Entity(parent=self, model='quad', scale=(2.0, 0.2), position=(0, 2.8, 0),
                                billboard=True, color=color.black)
        self.hp_bar = Entity(parent=self.hp_bar_bg, model='quad', scale=(1.0, 0.8), position=(0, 0, -0.01),
                             color=color.green)

    def trigger_recoil(self):
        # Анимация отката ствола и пороховой вспышки
        muzzle_world = self.barrel.world_position + self.turret.forward * 1.8
        spawn_fx(muzzle_world, scale=1.2, tint=color.yellow)
        self.barrel.z = 2.4
        self.barrel.animate('z', 2.8, duration=0.2, curve=curve.out_bounce)

    def update_hp_bar(self):
        ratio = max(0, self.hp / self.max_hp)
        self.hp_bar.scale_x = ratio
        if ratio > 0.5:
            self.hp_bar.color = color.green
        elif ratio > 0.25:
            self.hp_bar.color = color.orange
        else:
            self.hp_bar.color = color.red

# --- ТАНК ИГРОКА ---
class PlayerTank(DetailedTank):
    def __init__(self):
        super().__init__(pos=(0, 0, -35), camo_color=color.rgb(40, 110, 50))
        self.speed = 10
        self.turn_speed = 65
        self.shoot_cd = 0.0

    def update(self):
        if self.hp <= 0:
            return

        # Управление корпусом
        move = held_keys['w'] - held_keys['s']
        turn = held_keys['d'] - held_keys['a']

        self.rotation_y += turn * self.turn_speed * time.dt
        if move != 0:
            new_pos = self.position + self.forward * move * self.speed * time.dt
            if new_pos.length() < MAP_RADIUS - 3:
                self.position = new_pos

        # Наведение башни по курсору мыши (луч в плоскость Y = высоте башни)
        hit = mouse.world_point
        if hit:
            target = Vec3(hit.x, self.turret.world_y, hit.z)
            self.turret.look_at(target)

        # Камера третьего лица (следует плавно, не вращаясь рывками вместе с башней)
        target_cam_pos = self.position - self.forward * 14 + Vec3(0, 7.5, 0)
        camera.position = lerp(camera.position, target_cam_pos, time.dt * 6)
        camera.look_at(self.position + Vec3(0, 1.5, 0))

        # Стрельба
        if self.shoot_cd > 0:
            self.shoot_cd -= time.dt

        if held_keys['left mouse button'] and self.shoot_cd <= 0:
            self.shoot()

    def shoot(self):
        self.shoot_cd = 0.45
        self.trigger_recoil()
        muzzle_pos = self.barrel.world_position + self.turret.forward * 2.0
        Shell(muzzle_pos, self.turret.forward, is_player=True)

    def take_damage(self, dmg):
        self.hp -= dmg
        self.update_hp_bar()
        self.hull.color = color.red
        self.hull.animate_color(color.rgb(40, 110, 50), duration=0.2)
        if self.hp <= 0:
            spawn_fx(self.position, scale=5.0)
            ui_status.text = "ТАНК ПОДБИТ! Нажмите R для рестарта"
            ui_status.color = color.red

# --- ВРАЖЕСКИЙ ТАНК ---
enemies = []

class EnemyTank(DetailedTank):
    def __init__(self, pos):
        super().__init__(pos=pos, camo_color=color.rgb(140, 45, 45))
        self.speed = 4.5
        self.ai_cd = random.uniform(1.0, 2.5)
        self.shoot_cd = random.uniform(2.0, 4.0)
        self.target_dir = random.choice([0, 90, 180, 270])

    def update(self):
        if player.hp <= 0:
            return

        dist = distance(self.position, player.position)

        # Башня всегда держит игрока на мушке
        self.turret.look_at(player.position + Vec3(0, 1.2, 0))

        # Перемещение
        self.ai_cd -= time.dt
        if self.ai_cd <= 0:
            self.target_dir = random.choice([0, 90, 180, 270])
            self.ai_cd = random.uniform(2.0, 4.0)

        self.rotation_y = lerp(self.rotation_y, self.target_dir, time.dt * 2)
        if dist > 14:
            new_pos = self.position + self.forward * self.speed * time.dt
            if new_pos.length() < MAP_RADIUS - 4:
                self.position = new_pos

        # Огонь
        self.shoot_cd -= time.dt
        if self.shoot_cd <= 0 and dist < 50:
            self.shoot_cd = random.uniform(2.2, 4.0)
            self.trigger_recoil()
            muzzle_pos = self.barrel.world_position + self.turret.forward * 2.0
            Shell(muzzle_pos, self.turret.forward, is_player=False)

    def take_damage(self, dmg):
        self.hp -= dmg
        self.update_hp_bar()
        self.hull.color = color.white
        self.hull.animate_color(color.rgb(140, 45, 45), duration=0.2)
        if self.hp <= 0:
            spawn_fx(self.position, scale=4.0)
            if self in enemies:
                enemies.remove(self)
            destroy(self)
            global score
            score += 1
            ui_score.text = f"Уничтожено: {score}"

# Инициализация сущностей
player = PlayerTank()
camera.position = player.position + Vec3(0, 10, -15)

for ep in [(-30, 0, 25), (0, 0, 35), (30, 0, 20), (20, 0, 45)]:
    enemies.append(EnemyTank(pos=ep))

# Интерфейс
score = 0
ui_score = Text(text="Уничтожено: 0", position=(-0.85, 0.45), scale=1.4, color=color.yellow)
ui_status = Text(text="", position=(-0.4, 0.1), scale=1.8, color=color.red)
Text(text="W/S - Ход | A/D - Поворот корпуса | Мышь - Прицел | ЛКМ - Залп",
     position=(-0.85, -0.45), scale=1.0, color=color.white)

def input(key):
    if key == 'r' and player.hp <= 0:
        scene.clear()
        app.restart()

app.run()