Загрузка данных
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import math
import random
import sys
# --- ВЕКТОРНЫЕ ОПЕРАЦИИ ---
def v_len(v):
return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
def v_norm(v):
l = v_len(v)
return [0.0, 0.0, 0.0] if l < 1e-6 else [v[0]/l, v[1]/l, v[2]/l]
def v_dist(a, b):
return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2 + (a[2]-b[2])**2)
# --- ЖИВОЙ 3D РЕЛЬЕФ (ХОЛМЫ, ОВРАГИ, ВЫСОТЫ) ---
MAP_SIZE = 420.0
TERRAIN_STEP = 12
def get_terrain_height(x, z):
# Плавные холмы, низины и ложбины
h1 = math.sin(x * 0.025) * math.cos(z * 0.025) * 4.5
h2 = math.sin((x + z) * 0.015) * 3.0
h3 = math.cos(x * 0.05) * math.sin(z * 0.05) * 1.5
# Зона базы и спавна игрока сделаны ровнее
dist_player = math.sqrt(x*x + (z + 140)**2)
dist_base = math.sqrt(x*x + (z - 150)**2)
flatten = min(1.0, max(0.1, min(dist_player, dist_base) / 40.0))
return (h1 + h2 + h3) * flatten
# --- ГРАФИЧЕСКИЕ ПРИМИТИВЫ ---
def draw_box(size, color, offset=(0, 0, 0), emissive=(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])
glMaterialfv(GL_FRONT, GL_EMISSION, [*emissive, 1.0])
glBegin(GL_QUADS)
glNormal3f(0, 0, 1)
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)
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, 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, -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(1, 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)
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()
glMaterialfv(GL_FRONT, GL_EMISSION, [0.0, 0.0, 0.0, 1.0])
def draw_cylinder(radius, length, segments=14, 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 FloatingText:
def __init__(self, pos, text, color):
self.pos = [pos[0], pos[1] + 3.0, pos[2]]
self.text = text
self.color = color
self.life = 1.3
def update(self, dt):
self.pos[1] += 2.2 * dt
self.life -= dt
class ParticleSystem:
def __init__(self):
self.particles = []
self.floaters = []
def emit(self, pos, count=16, color=(1.0, 0.4, 0.0), speed=10.0, size=0.3):
for _ in range(count):
rx, ry, rz = random.uniform(-1, 1), random.uniform(0.3, 2.2), random.uniform(-1, 1)
norm = v_norm([rx, ry, rz])
sp = random.uniform(speed * 0.4, speed)
self.particles.append({
'x': pos[0], 'y': pos[1], 'z': pos[2],
'vx': norm[0] * sp, 'vy': norm[1] * sp, 'vz': norm[2] * sp,
'life': 1.0, 'decay': random.uniform(1.2, 2.5),
'color': color, 'size': size
})
def add_floating_text(self, pos, text, color):
self.floaters.append(FloatingText(pos, text, color))
def update(self, dt):
survivors = []
for p in self.particles:
p['x'] += p['vx'] * dt
p['y'] += p['vy'] * dt
p['z'] += p['vz'] * dt
p['vy'] -= 9.8 * dt * 0.6
p['life'] -= p['decay'] * dt
if p['life'] > 0:
survivors.append(p)
self.particles = survivors
self.floaters = [f for f in self.floaters if f.life > 0]
for f in self.floaters:
f.update(dt)
def draw(self):
glDisable(GL_LIGHTING)
for p in self.particles:
s = p['size'] * p['life']
glPushMatrix()
glTranslatef(p['x'], p['y'], p['z'])
draw_box((s, s, s), p['color'])
glPopMatrix()
glEnable(GL_LIGHTING)
# --- СНАРЯД ---
class Shell:
def __init__(self, pos, direction, team="player", base_dmg=35):
self.pos = list(pos)
self.dir = v_norm(direction)
self.speed = 135.0
self.team = team
self.base_dmg = base_dmg
self.active = True
self.life = 3.2
def update(self, dt):
self.pos[0] += self.dir[0] * self.speed * dt
self.pos[1] += self.dir[1] * self.speed * dt
self.pos[2] += self.dir[2] * self.speed * dt
self.life -= dt
# Проверка удара о рельеф
ground_y = get_terrain_height(self.pos[0], self.pos[2])
if self.pos[1] <= ground_y:
self.active = False
if self.life <= 0:
self.active = False
def draw(self):
glPushMatrix()
glTranslatef(self.pos[0], self.pos[1], self.pos[2])
if self.team == "player":
draw_box((0.35, 0.35, 1.4), (0.2, 0.85, 1.0), emissive=(0.2, 0.9, 1.0))
elif self.team == "ally":
draw_box((0.35, 0.35, 1.4), (0.2, 1.0, 0.4), emissive=(0.2, 1.0, 0.4))
else:
draw_box((0.45, 0.45, 1.4), (1.0, 0.1, 0.1), emissive=(1.0, 0.2, 0.1))
glPopMatrix()
# --- ОБЪЕКТЫ НА РЕЛЬЕФЕ ---
class Obstacle:
def __init__(self, x, z, obs_type):
self.pos = [float(x), get_terrain_height(x, z), float(z)]
self.obs_type = obs_type
self.hp = 120 if obs_type == "bunker" else (60 if obs_type == "tower" else 40)
self.alive = True
self.radius = 4.8 if obs_type == "bunker" else (2.8 if obs_type == "tower" else 2.0)
def check_collision(self, px, pz, r=2.0):
if not self.alive:
return False
return v_dist([px, 0, pz], [self.pos[0], 0, self.pos[2]]) < (self.radius + r)
def draw(self):
if not self.alive:
return
glPushMatrix()
glTranslatef(self.pos[0], self.pos[1], self.pos[2])
if self.obs_type == "bunker":
draw_box((7.4, 3.6, 7.4), (0.28, 0.30, 0.32), offset=(0, 1.8, 0))
draw_box((8.0, 0.6, 8.0), (0.18, 0.20, 0.22), offset=(0, 3.8, 0))
draw_box((3.6, 0.6, 7.6), (0.05, 0.05, 0.05), offset=(0, 2.2, 0))
elif self.obs_type == "tower":
for ox, oz in [(-1.5, -1.5), (1.5, -1.5), (-1.5, 1.5), (1.5, 1.5)]:
draw_box((0.5, 7.0, 0.5), (0.24, 0.18, 0.14), offset=(ox, 3.5, oz))
draw_box((3.8, 2.0, 3.8), (0.42, 0.28, 0.20), offset=(0, 7.8, 0))
draw_box((0.8, 0.8, 0.8), (1.0, 0.9, 0.3), offset=(0, 7.2, 2.0), emissive=(0.8, 0.7, 0.2))
else:
col = (0.2, 0.22, 0.25)
draw_box((0.4, 0.4, 3.5), col, offset=(0, 1.1, 0))
draw_box((3.5, 0.4, 0.4), col, offset=(0, 1.1, 0))
draw_box((0.4, 2.8, 0.4), col, offset=(0, 1.1, 0))
glPopMatrix()
# --- КЛАСС ДЕТАЛИЗИРОВАННОГО ТАНКА ---
class TankUnit:
def __init__(self, x, z, team="enemy", max_hp=100, speed=12.0, role="assault"):
self.pos = [float(x), get_terrain_height(x, z), float(z)]
self.rotation_y = 0.0
self.turret_angle = 0.0
self.pitch_angle = 0.0
self.recoil = 0.0
self.team = team
self.role = role
self.max_hp = float(max_hp)
self.hp = float(max_hp)
self.speed = speed
self.turn_speed = 95.0
self.shoot_cd = 0.0
self.max_shoot_cd = 0.55 if team == "player" else 2.8
self.alive = True
if team == "player":
self.c_hull = (0.16, 0.28, 0.42)
self.c_detail = (0.28, 0.48, 0.70)
self.c_glow = (0.1, 0.85, 1.0)
self.emissive = (0.0, 0.0, 0.0)
elif team == "ally":
self.c_hull = (0.18, 0.44, 0.22)
self.c_detail = (0.30, 0.65, 0.35)
self.c_glow = (0.2, 1.0, 0.4)
self.emissive = (0.02, 0.08, 0.02)
else:
self.c_hull = (0.85, 0.12, 0.12)
self.c_detail = (1.0, 0.28, 0.18)
self.c_glow = (1.0, 0.05, 0.0)
self.emissive = (0.35, 0.02, 0.02)
def get_forward_vector(self):
rad = math.radians(self.rotation_y)
return [math.sin(rad), 0.0, math.cos(rad)]
def get_turret_vector(self):
rad_y = math.radians(self.rotation_y + self.turret_angle)
rad_p = math.radians(-self.pitch_angle)
return [
math.sin(rad_y) * math.cos(rad_p),
math.sin(rad_p),
math.cos(rad_y) * math.cos(rad_p)
]
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 * 4.5)
# Привязка высоты к текущему холму
self.pos[1] = get_terrain_height(self.pos[0], self.pos[2])
def take_hit(self, incoming_dmg, hit_vector):
spread = random.uniform(0.80, 1.20)
dmg = int(incoming_dmg * spread)
is_crit = random.random() < 0.15
if is_crit:
dmg = int(dmg * 1.5)
self.hp = max(0.0, self.hp - dmg)
return dmg, f"КРИТ! -{dmg}"
self.hp = max(0.0, self.hp - dmg)
return dmg, f"-{dmg}"
def draw(self, skip_hull=False):
if not self.alive:
return
glPushMatrix()
glTranslatef(self.pos[0], self.pos[1], self.pos[2])
glRotatef(self.rotation_y, 0, 1, 0)
# Рисуем корпус (если не включен вид из снайперского прицела)
if not skip_hull:
# Гусеницы и катки
c_tr = (0.12, 0.12, 0.14)
draw_box((0.85, 0.75, 4.8), c_tr, offset=(-1.45, 0.38, 0.0))
draw_box((0.85, 0.75, 4.8), c_tr, offset=(1.45, 0.38, 0.0))
draw_box((0.9, 0.25, 5.0), self.c_detail, offset=(-1.45, 0.8, 0.0), emissive=self.emissive)
draw_box((0.9, 0.25, 5.0), self.c_detail, offset=(1.45, 0.8, 0.0), emissive=self.emissive)
# Корпус
draw_box((2.5, 0.7, 4.3), self.c_hull, offset=(0.0, 0.65, 0.0), emissive=self.emissive)
draw_box((2.2, 0.4, 3.8), self.c_detail, offset=(0.0, 1.0, -0.1), emissive=self.emissive)
draw_box((2.1, 0.3, 1.2), self.c_hull, offset=(0.0, 0.75, 1.8), emissive=self.emissive)
# Башня и пушка (рисуются всегда!)
glPushMatrix()
glTranslatef(0.0, 1.3, 0.0)
glRotatef(self.turret_angle, 0, 1, 0)
if not skip_hull:
draw_box((2.1, 0.75, 2.6), self.c_hull, offset=(0.0, 0.0, -0.2), emissive=self.emissive)
draw_box((1.7, 0.25, 2.2), self.c_detail, offset=(0.0, 0.42, -0.2), emissive=self.emissive)
draw_box((0.25, 0.25, 0.25), self.c_glow, offset=(0.55, 0.75, -0.4), emissive=self.c_glow)
# Орудие, маска и дульный тормоз с углами наклона УВН
glPushMatrix()
glTranslatef(0.0, 0.05, 1.1)
glRotatef(self.pitch_angle, 1, 0, 0)
glTranslatef(0.0, 0.0, -self.recoil)
draw_box((1.1, 0.55, 0.65), (0.1, 0.1, 0.1))
draw_cylinder(0.16, 3.6, segments=14, color=(0.14, 0.14, 0.14))
draw_box((0.48, 0.48, 0.7), (0.18, 0.18, 0.18), offset=(0.0, 0.0, 3.6))
glPopMatrix()
glPopMatrix() # Башня
glPopMatrix() # Танк
# Четкая шкала HP над врагами и союзниками
if self.alive and self.team != "player":
self.draw_health_bar()
def draw_health_bar(self):
glDisable(GL_LIGHTING)
glDisable(GL_DEPTH_TEST) # Рисуем строго поверх геометрии
glPushMatrix()
glTranslatef(self.pos[0], self.pos[1] + 4.2, self.pos[2])
mat = glGetFloatv(GL_MODELVIEW_MATRIX)
for i in range(3):
for j in range(3):
mat[i][j] = 1.0 if i == j else 0.0
glLoadMatrixf(mat)
pct = max(0.0, min(1.0, self.hp / self.max_hp))
# Подложка
draw_box((2.6, 0.26, 0.02), (0.05, 0.05, 0.05))
# Цветная полоска здоровья (уменьшается в реальном времени)
bar_col = (0.2, 0.9, 0.3) if self.team == "ally" else ((1.0, 0.15, 0.15) if pct < 0.4 else (1.0, 0.5, 0.1))
cur_w = 2.52 * pct
x_off = -1.26 + (cur_w / 2.0)
draw_box((cur_w, 0.20, 0.04), bar_col, offset=(x_off, 0.0, 0.01))
glPopMatrix()
glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
# --- ОСНОВНОЙ ДВИЖОК ИГРЫ ---
class TankWarGame:
def __init__(self):
pygame.init()
self.res = (1280, 720)
pygame.display.set_mode(self.res, DOUBLEBUF | OPENGL)
pygame.display.set_caption("World of Steel: Platoon Assault 3D")
pygame.mouse.set_visible(False)
pygame.event.set_grab(True)
self.clock = pygame.time.Clock()
self.particles = ParticleSystem()
self.map_size = MAP_SIZE
self.score = 0
self.game_over = False
self.victory = False
self.enemy_base_pos = [0.0, get_terrain_height(0, 150), 150.0]
self.base_radius = 30.0
self.base_capture = 0.0
self.aim_mode = False
self.init_gl()
self.init_world()
def init_gl(self):
glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glLightfv(GL_LIGHT0, GL_POSITION, [150.0, 220.0, 120.0, 1.0])
glLightfv(GL_LIGHT0, GL_DIFFUSE, [1.0, 0.98, 0.95, 1.0])
glLightfv(GL_LIGHT0, GL_AMBIENT, [0.38, 0.38, 0.42, 1.0])
glClearColor(0.24, 0.32, 0.42, 1.0)
def set_camera_fov(self, fov):
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(fov, (self.res[0] / self.res[1]), 0.4, 650.0)
glMatrixMode(GL_MODELVIEW)
def init_world(self):
self.player = TankUnit(0, -140, team="player", max_hp=200, speed=16.0)
# 2 союзника (взвод)
self.allies = [
TankUnit(-18, -155, team="ally", max_hp=150, speed=14.0),
TankUnit(18, -155, team="ally", max_hp=150, speed=14.0)
]
# Вражеский отряд
self.enemies = []
spawn_data = [
(-50, 100, "scout", 90, 13.0),
(50, 100, "scout", 90, 13.0),
(0, 130, "assault", 140, 10.0),
(-80, 150, "heavy", 240, 7.5),
(80, 150, "heavy", 240, 7.5),
(0, 180, "heavy", 260, 7.0)
]
for ex, ez, role, hp, sp in spawn_data:
self.enemies.append(TankUnit(ex, ez, team="enemy", max_hp=hp, speed=sp, role=role))
self.bullets = []
self.obstacles = []
random.seed(42)
for _ in range(75):
ox = random.uniform(-self.map_size/2 + 35, self.map_size/2 - 35)
oz = random.uniform(-self.map_size/2 + 35, self.map_size/2 - 35)
if v_dist([ox, 0, oz], self.player.pos) > 30 and v_dist([ox, 0, oz], self.enemy_base_pos) > 40:
t = random.choices(["bunker", "tower", "hedgehog"], weights=[45, 30, 25])[0]
self.obstacles.append(Obstacle(ox, oz, t))
self.cam_yaw = 0.0
self.cam_pitch = 18.0
self.cam_dist = 18.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 == MOUSEBUTTONDOWN:
if event.button == 3:
self.aim_mode = True
if event.button == 1:
if self.player.alive and self.player.shoot_cd <= 0:
self.fire_tank(self.player, base_dmg=50)
if event.type == MOUSEBUTTONUP:
if event.button == 3:
self.aim_mode = False
if event.type == MOUSEMOTION:
dx, dy = event.rel
sens = 0.07 if self.aim_mode else 0.22
self.cam_yaw -= dx * sens
# Свободный вертикальный ход прицела вверх/вниз
self.cam_pitch = max(-35.0, min(50.0, self.cam_pitch + dy * (sens * 0.8)))
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.65
if move_dir != 0:
fwd = self.player.get_forward_vector()
nx = self.player.pos[0] + fwd[0] * (move_dir * self.player.speed * dt)
nz = self.player.pos[2] + fwd[2] * (move_dir * self.player.speed * dt)
limit = self.map_size / 2.0 - 6.0
if abs(nx) < limit and abs(nz) < limit:
blocked = any(obs.check_collision(nx, nz, 2.3) for obs in self.obstacles)
if not blocked:
all_tanks = self.allies + self.enemies
blocked = any(t.alive and v_dist([nx, 0, nz], [t.pos[0], 0, t.pos[2]]) < 3.8 for t in all_tanks)
if not blocked:
self.player.pos[0] = nx
self.player.pos[2] = nz
target_turret = (self.cam_yaw - self.player.rotation_y) % 360
if target_turret > 180:
target_turret -= 360
self.player.turret_angle = target_turret
self.player.pitch_angle = self.cam_pitch
def fire_tank(self, tank, base_dmg=35):
tank.shoot_cd = tank.max_shoot_cd
tank.recoil = 0.55
fwd = tank.get_turret_vector()
spawn_pos = [
tank.pos[0] + fwd[0] * 4.8,
tank.pos[1] + 1.35 + fwd[1] * 4.8,
tank.pos[2] + fwd[2] * 4.8
]
self.bullets.append(Shell(spawn_pos, fwd, team=tank.team, base_dmg=base_dmg))
col = (0.2, 0.85, 1.0) if tank.team == "player" else ((0.2, 1.0, 0.4) if tank.team == "ally" else (1.0, 0.25, 0.1))
self.particles.emit(spawn_pos, count=18, color=col, speed=8.0, size=0.28)
def update_bots(self, dt):
# Союзники следуют строем
offsets = [(-16, -14), (16, -14)]
for i, ally in enumerate(self.allies):
if not ally.alive:
continue
target_slot = [
self.player.pos[0] + offsets[i][0],
0.0,
self.player.pos[2] + offsets[i][1]
]
d_to_slot = v_dist(ally.pos, target_slot)
if d_to_slot > 6.0:
to_s = [target_slot[0] - ally.pos[0], 0, target_slot[2] - ally.pos[2]]
want_yaw = math.degrees(math.atan2(to_s[0], to_s[2]))
diff = (want_yaw - ally.rotation_y) % 360
if diff > 180: diff -= 360
ally.rotation_y += math.copysign(min(abs(diff), ally.turn_speed * dt), diff)
fwd = ally.get_forward_vector()
ally.pos[0] += fwd[0] * ally.speed * dt
ally.pos[2] += fwd[2] * ally.speed * dt
live_enemies = [e for e in self.enemies if e.alive]
if live_enemies:
nearest_e = min(live_enemies, key=lambda e: v_dist(ally.pos, e.pos))
if v_dist(ally.pos, nearest_e.pos) < 140.0:
to_e = [nearest_e.pos[0] - ally.pos[0], 0, nearest_e.pos[2] - ally.pos[2]]
target_yaw = math.degrees(math.atan2(to_e[0], to_e[2]))
rel_yaw = (target_yaw - ally.rotation_y) % 360
if rel_yaw > 180: rel_yaw -= 360
ally.turret_angle = rel_yaw
if ally.shoot_cd <= 0:
self.fire_tank(ally, base_dmg=35)
ally.update(dt)
# Враги
targets = ([self.player] if self.player.alive else []) + [a for a in self.allies if a.alive]
for e in self.enemies:
if not e.alive:
continue
if targets:
target = min(targets, key=lambda t: v_dist(e.pos, t.pos))
dist = v_dist(e.pos, target.pos)
to_t = [target.pos[0] - e.pos[0], 0, target.pos[2] - e.pos[2]]
target_yaw = math.degrees(math.atan2(to_t[0], to_t[2]))
rel_yaw = (target_yaw - e.rotation_y) % 360
if rel_yaw > 180: rel_yaw -= 360
e.turret_angle = rel_yaw
sign = 1.0 if rel_yaw > 0 else -1.0
e.rotation_y += sign * e.turn_speed * 0.45 * dt
if dist > 32.0:
fwd = e.get_forward_vector()
e.pos[0] += fwd[0] * e.speed * dt
e.pos[2] += fwd[2] * e.speed * dt
if dist < 120.0 and e.shoot_cd <= 0:
dmg = 45 if e.role == "heavy" else 30
self.fire_tank(e, base_dmg=dmg)
e.update(dt)
def update_physics(self, dt):
self.player.update(dt)
self.update_bots(dt)
self.particles.update(dt)
# Захват вражеской базы
if self.player.alive and v_dist(self.player.pos, self.enemy_base_pos) < self.base_radius:
self.base_capture = min(100.0, self.base_capture + dt * 10.0)
if self.base_capture >= 100.0:
self.victory = True
self.game_over = True
else:
self.base_capture = max(0.0, self.base_capture - dt * 5.0)
surviving_shells = []
all_units = [self.player] + self.allies + self.enemies
for b in self.bullets:
b.update(dt)
if not b.active:
continue
hit = False
for obs in self.obstacles:
if obs.check_collision(b.pos[0], b.pos[2], 0.7):
obs.hp -= b.base_dmg
self.particles.emit(b.pos, count=12, color=(0.5, 0.45, 0.4), speed=6.0)
if obs.hp <= 0:
obs.alive = False
self.particles.emit(obs.pos, count=45, color=(0.3, 0.3, 0.3), speed=12.0, size=0.6)
hit = True
break
if hit:
continue
for u in all_units:
if not u.alive:
continue
if (b.team in ["player", "ally"] and u.team in ["player", "ally"]) or (b.team == "enemy" and u.team == "enemy"):
continue
if v_dist(b.pos, [u.pos[0], u.pos[1] + 1.2, u.pos[2]]) < 2.8:
dmg_dealt, text_label = u.take_hit(b.base_dmg, b.dir)
txt_col = (1.0, 0.9, 0.2) if "КРИТ" in text_label else ((0.9, 0.2, 0.2) if dmg_dealt > 0 else (0.7, 0.7, 0.7))
self.particles.add_floating_text(u.pos, text_label, txt_col)
self.particles.emit(b.pos, count=24, color=(1.0, 0.4, 0.1), speed=12.0)
if u.hp <= 0:
u.alive = False
self.particles.emit(u.pos, count=85, color=(1.0, 0.25, 0.05), speed=16.0, size=0.9)
if u.team == "enemy":
self.score += 200
elif u == self.player:
self.game_over = True
self.victory = False
hit = True
break
if not hit:
surviving_shells.append(b)
self.bullets = surviving_shells
if all(not e.alive for e in self.enemies):
self.victory = True
self.game_over = True
def render_base(self):
glDisable(GL_LIGHTING)
glColor3f(1.0, 0.2, 0.2)
glLineWidth(3.0)
glBegin(GL_LINE_LOOP)
for i in range(36):
rad = 2.0 * math.pi * i / 36
x = self.enemy_base_pos[0] + math.cos(rad) * self.base_radius
z = self.enemy_base_pos[2] + math.sin(rad) * self.base_radius
y = get_terrain_height(x, z) + 0.3
glVertex3f(x, y, z)
glEnd()
glEnable(GL_LIGHTING)
glPushMatrix()
glTranslatef(self.enemy_base_pos[0], self.enemy_base_pos[1], self.enemy_base_pos[2])
draw_cylinder(0.3, 14.0, segments=10, color=(0.3, 0.3, 0.3))
draw_box((4.0, 2.5, 0.2), (0.9, 0.1, 0.1), offset=(2.0, 12.0, 0))
glPopMatrix()
def render_terrain(self):
# Отрисовка неровной холмистой сетки
step = TERRAIN_STEP
half = int(self.map_size / 2)
glBegin(GL_TRIANGLES)
for x in range(-half, half, step):
for z in range(-half, half, step):
y00 = get_terrain_height(x, z)
y10 = get_terrain_height(x + step, z)
y11 = get_terrain_height(x + step, z + step)
y01 = get_terrain_height(x, z + step)
c = (0.24, 0.26, 0.27) if (x // step + z // step) % 2 == 0 else (0.20, 0.22, 0.23)
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, [*c, 1.0])
# Нормали граней треугольников
glNormal3f(0, 1, 0)
glVertex3f(x, y00, z)
glVertex3f(x + step, y10, z)
glVertex3f(x + step, y11, z + step)
glVertex3f(x, y00, z)
glVertex3f(x + step, y11, z + step)
glVertex3f(x, y01, z + step)
glEnd()
def draw_hud(self):
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
glOrtho(0, self.res[0], self.res[1], 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
glDisable(GL_LIGHTING)
glDisable(GL_DEPTH_TEST)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
cx, cy = self.res[0] // 2, self.res[1] // 2
# 1. ПРИЦЕЛ
if self.aim_mode:
# Снайперское перекрестие
glColor3f(0.2, 1.0, 0.4)
glLineWidth(1.8)
glBegin(GL_LINES)
glVertex2f(cx - 180, cy); glVertex2f(cx + 180, cy)
glVertex2f(cx, cy - 140); glVertex2f(cx, cy + 140)
for t in [-100, -50, 50, 100]:
glVertex2f(cx + t, cy - 8); glVertex2f(cx + t, cy + 8)
glVertex2f(cx - 8, cy + t); glVertex2f(cx + 8, cy + t)
glEnd()
z_font = pygame.font.SysFont("impact", 20)
z_surf = z_font.render("SNIPER OPTICS 4.0X", True, (50, 255, 100))
glRasterPos2i(cx - z_surf.get_width() // 2, cy - 160)
glDrawPixels(z_surf.get_width(), z_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, pygame.image.tostring(z_surf, "RGBA", True))
else:
r = 16
cd_pct = max(0.0, self.player.shoot_cd / self.player.max_shoot_cd)
col = (1.0, 0.2, 0.2) if cd_pct > 0 else (0.2, 0.9, 1.0)
glColor3f(*col)
glLineWidth(2.0)
glBegin(GL_LINES)
glVertex2f(cx - r, cy); glVertex2f(cx - 5, cy)
glVertex2f(cx + 5, cy); glVertex2f(cx + r, cy)
glVertex2f(cx, cy - r); glVertex2f(cx, cy - 5)
glVertex2f(cx, cy + 5); glVertex2f(cx, cy + r)
glEnd()
# 2. ПОЛОСА ЗАХВАТА БАЗЫ
if self.base_capture > 0:
bw, bh = 300, 22
bx, by = cx - bw // 2, 85
glColor4f(0.1, 0.1, 0.1, 0.8)
glBegin(GL_QUADS)
glVertex2f(bx, by); glVertex2f(bx + bw, by)
glVertex2f(bx + bw, by + bh); glVertex2f(bx, by + bh)
glEnd()
glColor3f(0.2, 0.8, 1.0)
cap_w = bw * (self.base_capture / 100.0)
glBegin(GL_QUADS)
glVertex2f(bx, by); glVertex2f(bx + cap_w, by)
glVertex2f(bx + cap_w, by + bh); glVertex2f(bx, by + bh)
glEnd()
c_font = pygame.font.SysFont("impact", 18)
c_surf = c_font.render(f"BASE CAPTURE: {int(self.base_capture)}%", True, (255, 255, 255))
glRasterPos2i(cx - c_surf.get_width() // 2, by - 5)
glDrawPixels(c_surf.get_width(), c_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, pygame.image.tostring(c_surf, "RGBA", True))
# 3. НИЖНЯЯ ПАНЕЛЬ HP ИГРОКА
panel_w, panel_h = 440, 60
px = cx - panel_w // 2
py = self.res[1] - 80
glColor4f(0.06, 0.08, 0.12, 0.9)
glBegin(GL_QUADS)
glVertex2f(px, py); glVertex2f(px + panel_w, py)
glVertex2f(px + panel_w, py + panel_h); glVertex2f(px, py + panel_h)
glEnd()
bar_x, bar_y = px + 15, py + 22
max_bw = panel_w - 30
hp_pct = max(0.0, self.player.hp / self.player.max_hp)
cur_bw = max_bw * hp_pct
hp_color = (0.2, 0.9, 0.4) if hp_pct > 0.5 else ((0.95, 0.6, 0.1) if hp_pct > 0.25 else (0.9, 0.15, 0.15))
glColor3f(*hp_color)
glBegin(GL_QUADS)
glVertex2f(bar_x, bar_y); glVertex2f(bar_x + cur_bw, bar_y)
glVertex2f(bar_x + cur_bw, bar_y + 18); glVertex2f(bar_x, bar_y + 18)
glEnd()
# 4. РАДАР
radar_size = 150
rx = self.res[0] - radar_size - 25
ry = 25
glColor4f(0.05, 0.07, 0.09, 0.9)
glBegin(GL_QUADS)
glVertex2f(rx, ry); glVertex2f(rx + radar_size, ry)
glVertex2f(rx + radar_size, ry + radar_size); glVertex2f(rx, ry + radar_size)
glEnd()
def to_radar(wx, wz):
nx = (wx / (self.map_size / 2.0)) * (radar_size / 2.0)
nz = (wz / (self.map_size / 2.0)) * (radar_size / 2.0)
return rx + radar_size / 2.0 + nx, ry + radar_size / 2.0 + nz
bx_r, by_r = to_radar(self.enemy_base_pos[0], self.enemy_base_pos[2])
glColor3f(1.0, 0.2, 0.2)
glLineWidth(2.0)
glBegin(GL_LINE_LOOP)
for i in range(16):
rad = 2.0 * math.pi * i / 16
glVertex2f(bx_r + math.cos(rad) * 10, by_r + math.sin(rad) * 10)
glEnd()
glPointSize(4.0)
glBegin(GL_POINTS)
glColor3f(1.0, 0.1, 0.1)
for e in self.enemies:
if e.alive:
mx, my = to_radar(e.pos[0], e.pos[2])
glVertex2f(mx, my)
glColor3f(0.2, 0.9, 0.3)
for a in self.allies:
if a.alive:
mx, my = to_radar(a.pos[0], a.pos[2])
glVertex2f(mx, my)
glColor3f(0.1, 0.8, 1.0)
px_r, py_r = to_radar(self.player.pos[0], self.player.pos[2])
glVertex2f(px_r, py_r)
glEnd()
# ТЕКСТЫ И ВСПЛЫВАЮЩИЙ УРОН
font = pygame.font.SysFont("impact", 22)
live_allies = sum(1 for a in self.allies if a.alive) + (1 if self.player.alive else 0)
live_enemies = sum(1 for e in self.enemies if e.alive)
hud_text = f"PLATOON: {live_allies}/3 ENEMIES: {live_enemies} SCORE: {self.score}"
surf = font.render(hud_text, True, (240, 240, 240))
glRasterPos2i(30, 45)
glDrawPixels(surf.get_width(), surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, pygame.image.tostring(surf, "RGBA", True))
hp_text = f"COMMANDER HP: {int(self.player.hp)} / {int(self.player.max_hp)} (RMB - SNIPER MODE)"
hp_surf = pygame.font.SysFont("consolas", 12, bold=True).render(hp_text, True, (255, 255, 255))
glRasterPos2i(bar_x + 8, py + 14)
glDrawPixels(hp_surf.get_width(), hp_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, pygame.image.tostring(hp_surf, "RGBA", True))
# Окно победы/поражения
if self.game_over:
m_font = pygame.font.SysFont("impact", 54)
msg = "TACTICAL VICTORY!" if self.victory else "DEFEAT - PLATOON DOWN!"
col = (40, 255, 40) if self.victory else (255, 40, 40)
m_surf = m_font.render(msg, True, col)
glRasterPos2i(cx - m_surf.get_width() // 2, cy - 40)
glDrawPixels(m_surf.get_width(), m_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, pygame.image.tostring(m_surf, "RGBA", True))
sub_font = pygame.font.SysFont("consolas", 20, bold=True)
s_surf = sub_font.render("Press R to Redeploy Platoon | ESC to Exit", True, (255, 255, 255))
glRasterPos2i(cx - s_surf.get_width() // 2, cy + 20)
glDrawPixels(s_surf.get_width(), s_surf.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, pygame.image.tostring(s_surf, "RGBA", True))
glDisable(GL_BLEND)
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)
target_fov = 18.0 if self.aim_mode else 60.0
self.set_camera_fov(target_fov)
glLoadIdentity()
rad_yaw = math.radians(self.cam_yaw)
rad_pitch = math.radians(self.cam_pitch)
if self.aim_mode:
# В прицеле: камера смещена вперед по оси ствола
cam_x = self.player.pos[0] + math.sin(rad_yaw) * 1.8
cam_y = self.player.pos[1] + 1.85
cam_z = self.player.pos[2] + math.cos(rad_yaw) * 1.8
target_x = cam_x + math.sin(rad_yaw) * math.cos(rad_pitch) * 50.0
target_y = cam_y - math.sin(rad_pitch) * 50.0
target_z = cam_z + math.cos(rad_yaw) * math.cos(rad_pitch) * 50.0
gluLookAt(cam_x, cam_y, cam_z, target_x, target_y, target_z, 0, 1, 0)
else:
# Вид от 3-го лица: сзади танка
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) + 2.6
cam_z = self.player.pos[2] - self.cam_dist * math.cos(rad_yaw) * math.cos(rad_pitch)
target_y = self.player.pos[1] + 1.3
gluLookAt(cam_x, cam_y, cam_z, self.player.pos[0], target_y, self.player.pos[2], 0, 1, 0)
# Рендеринг сцены
self.render_terrain()
self.render_base()
for obs in self.obstacles:
obs.draw()
# В снайперском режиме скрывается только корпус танка, башня и пушка отрисовываются
self.player.draw(skip_hull=self.aim_mode)
for ally in self.allies:
ally.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()