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


from ursina import *
import random

app = Ursina()

# --- ОКРУЖЕНИЕ И ОСВЕЩЕНИЕ ---
window.title = "3D Tanks Prototype"
window.borderless = False
window.fullscreen = False
window.fps_counter.enabled = True

# Большая карта (арена 120x120)
MAP_SIZE = 120
ground = Entity(
    model='plane',
    scale=(MAP_SIZE, 1, MAP_SIZE),
    color=color.rgb(60, 70, 50),
    texture='white_cube',
    texture_scale=(MAP_SIZE, MAP_SIZE),
    collider='box'
)

DirectionalLight(y=10, rotation=(45, -45, 45), shadows=False)
AmbientLight(color=color.rgba(120, 120, 120, 255))

# Границы карты (невидимые стены)
for x, z, sx, sz in [
    (0, MAP_SIZE/2, MAP_SIZE, 2),
    (0, -MAP_SIZE/2, MAP_SIZE, 2),
    (MAP_SIZE/2, 0, 2, MAP_SIZE),
    (-MAP_SIZE/2, 0, 2, MAP_SIZE)
]:
    Entity(model='cube', position=(x, 2, z), scale=(sx, 4, sz), collider='box', visible=False)

# Разрушаемые укрытия и препятствия
obstacles = []
for _ in range(35):
    ox = random.uniform(-MAP_SIZE/2 + 10, MAP_SIZE/2 - 10)
    oz = random.uniform(-MAP_SIZE/2 + 10, MAP_SIZE/2 - 10)
    if abs(ox) > 8 or abs(oz) > 8:  # освобождаем спавн
        obs = Entity(
            model='cube',
            position=(ox, 1.5, oz),
            scale=(random.choice([3, 4]), 3, random.choice([3, 4])),
            color=color.rgb(130, 90, 60),
            texture='white_cube',
            collider='box'
        )
        obs.hp = 2
        obstacles.append(obs)

# --- ПУЛИ И ВЗРЫВЫ ---
bullets = []

def create_explosion(pos, color_tint=color.orange):
    exp = Entity(model='sphere', position=pos, scale=0.5, color=color_tint)
    exp.animate_scale(3.5, duration=0.25, curve=curve.out_expo)
    exp.animate_color(color.clear, duration=0.25)
    destroy(exp, delay=0.3)

class Shell(Entity):
    def __init__(self, pos, direction, is_player=True):
        super().__init__(
            model='sphere',
            scale=0.3,
            color=color.yellow if is_player else color.red,
            position=pos,
            collider='sphere'
        )
        self.direction = direction
        self.is_player = is_player
        self.lifetime = 3.0
        bullets.append(self)

    def update(self):
        self.position += self.direction * 50 * time.dt
        self.lifetime -= time.dt
        if self.lifetime <= 0:
            bullets.remove(self)
            destroy(self)
            return

        hit_info = self.intersects()
        if hit_info.hit and hit_info.entity != ground:
            ent = hit_info.entity
            # Попадание в укрытия
            if ent in obstacles:
                ent.hp -= 1
                create_explosion(self.position, color.gray)
                if ent.hp <= 0:
                    obstacles.remove(ent)
                    destroy(ent)
                self.destroy_shell()
                return

            # Попадание по врагу
            if self.is_player and isinstance(ent, EnemyTank):
                ent.take_damage(1)
                self.destroy_shell()
                return

            # Попадание по игроку
            if not self.is_player and ent in [player.hull, player.turret, player.chassis]:
                player.take_damage(1)
                self.destroy_shell()
                return

    def destroy_shell(self):
        create_explosion(self.position)
        if self in bullets:
            bullets.remove(self)
        destroy(self)

# --- БАЗОВАЯ МОДЕЛЬ ТАНКА ---
class BaseTank(Entity):
    def __init__(self, pos=(0, 0, 0), primary_color=color.rgb(40, 110, 40)):
        super().__init__(position=pos)

        # Гусеничная база
        self.chassis = Entity(parent=self, model='cube', scale=(2.6, 0.5, 3.8), y=0.25, color=color.dark_gray)
        # Корпус
        self.hull = Entity(parent=self, model='cube', scale=(2.4, 0.7, 3.4), y=0.7, color=primary_color)
        # Башня
        self.turret = Entity(parent=self, model='cube', scale=(1.6, 0.6, 1.8), y=1.2, color=primary_color)
        # Дуло
        self.cannon = Entity(parent=self.turret, model='cylinder', scale=(0.25, 2.5, 0.25),
                             rotation_x=90, z=1.4, y=0.0, color=color.black)

        self.recoil_anim = False
        self.hp = 3

    def trigger_recoil(self):
        # Анимация отдачи ствола
        self.cannon.z = 0.9
        self.cannon.animate('z', 1.4, duration=0.15, curve=curve.out_bounce)

# --- ИГРОК С КАМЕРОЙ ОТ 3-ГО ЛИЦА ---
class PlayerTank(BaseTank):
    def __init__(self):
        super().__init__(pos=(0, 0, -30), primary_color=color.rgb(45, 120, 60))
        self.speed = 9
        self.turn_speed = 70
        self.shoot_cd = 0.0

        # Камера 3-го лица
        camera.parent = self
        camera.position = (0, 7, -13)
        camera.rotation_x = 22

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

        # Управление движением (W/S — вперед/назад, A/D — поворот корпуса)
        move_dir = held_keys['w'] - held_keys['s']
        turn_dir = held_keys['d'] - held_keys['a']

        self.rotation_y += turn_dir * self.turn_speed * time.dt
        if move_dir != 0:
            next_pos = self.position + self.forward * move_dir * self.speed * time.dt
            # Ограничение по границам арены
            if abs(next_pos.x) < MAP_SIZE/2 - 2 and abs(next_pos.z) < MAP_SIZE/2 - 2:
                self.position = next_pos

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

        if self.shoot_cd > 0:
            self.shoot_cd -= time.dt

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

    def fire(self):
        self.shoot_cd = 0.5
        self.trigger_recoil()
        barrel_tip = self.cannon.world_position + self.turret.forward * 1.5
        Shell(barrel_tip, self.turret.forward, is_player=True)

    def take_damage(self, dmg):
        self.hp -= dmg
        self.hull.color = color.red
        self.hull.animate_color(color.rgb(45, 120, 60), duration=0.2)
        if self.hp <= 0:
            create_explosion(self.position)
            print_on_screen("ТАНК УНИЧТОЖЕН! Нажмите R для перезапуска", position=(-0.4, 0), scale=2, duration=10)

player = PlayerTank()

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

class EnemyTank(BaseTank):
    def __init__(self, pos):
        super().__init__(pos=pos, primary_color=color.rgb(150, 40, 40))
        self.collider = 'box'
        self.speed = 4
        self.ai_timer = random.uniform(1.0, 3.0)
        self.shoot_timer = random.uniform(2.0, 4.0)
        self.target_dir = random.choice([0, 90, 180, 270])

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

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

        # Поведение: смотрим башней на игрока
        self.turret.look_at(player.position + Vec3(0, 1, 0))

        # Патрулирование / приближение
        self.ai_timer -= time.dt
        if self.ai_timer <= 0:
            self.target_dir = random.choice([0, 90, 180, 270])
            self.ai_timer = random.uniform(2.0, 4.0)

        self.rotation_y = lerp(self.rotation_y, self.target_dir, time.dt * 2)
        if dist_to_player > 12:
            self.position += self.forward * self.speed * time.dt

        # Стрельба в игрока
        self.shoot_timer -= time.dt
        if self.shoot_timer <= 0 and dist_to_player < 40:
            self.shoot_timer = random.uniform(2.5, 4.5)
            self.trigger_recoil()
            barrel_tip = self.cannon.world_position + self.turret.forward * 1.5
            Shell(barrel_tip, self.turret.forward, is_player=False)

    def take_damage(self, dmg):
        self.hp -= dmg
        self.hull.color = color.white
        self.hull.animate_color(color.rgb(150, 40, 40), duration=0.2)
        if self.hp <= 0:
            create_explosion(self.position)
            if self in enemies:
                enemies.remove(self)
            destroy(self)

# Спавн группы врагов
for _ in range(5):
    ex = random.choice([-35, -20, 20, 35])
    ez = random.choice([10, 20, 30, 40])
    enemies.append(EnemyTank(pos=(ex, 0, ez)))

# --- СИСТЕМА ПЕРЕЗАПУСКА ---
def input(key):
    if key == 'r' and player.hp <= 0:
        # Перезапуск сцены
        scene.clear()
        app.restart()

# Интерфейс
Text("Управление: W/A/S/D - Движение корпуса, Мышь - Прицел, ЛКМ - Выстрел", position=(-0.85, 0.45), scale=1)

app.run()