Upload files to "/"

main
Gļebs Cvetkovs 2024-04-12 07:49:10 +00:00
parent 7bf027b495
commit 61ef39e105
5 changed files with 509 additions and 0 deletions

104
player.py 100644
View File

@ -0,0 +1,104 @@
from settings import *
import pygame
import math
from map import collision_walls
class Player:
def __init__(self, sprites):
self.x, self.y = player_pos
self.sprites = sprites
self.angle = player_angle
self.sensitivity = 0.004
# collision parameters
self.side = 50
self.rect = pygame.Rect(*player_pos, self.side, self.side)
# weapon
self.shot = False
self.health = PLAYER_HEALTH
def get_damage(self, damage):
self.health -= damage
@property
def pos(self):
return (self.x, self.y)
@property
def collision_list(self):
return collision_walls + [pygame.Rect(*obj.pos, obj.side, obj.side) for obj in
self.sprites.list_of_objects if obj.blocked]
def detect_collision(self, dx, dy):
next_rect = self.rect.copy()
next_rect.move_ip(dx, dy)
hit_indexes = next_rect.collidelistall(self.collision_list)
if len(hit_indexes):
delta_x, delta_y = 0, 0
for hit_index in hit_indexes:
hit_rect = self.collision_list[hit_index]
if dx > 0:
delta_x += next_rect.right - hit_rect.left
else:
delta_x += hit_rect.right - next_rect.left
if dy > 0:
delta_y += next_rect.bottom - hit_rect.top
else:
delta_y += hit_rect.bottom - next_rect.top
if abs(delta_x - delta_y) < 10:
dx, dy = 0, 0
elif delta_x > delta_y:
dy = 0
elif delta_y > delta_x:
dx = 0
self.x += dx
self.y += dy
def movement(self):
self.keys_control()
self.mouse_control()
self.rect.center = self.x, self.y
self.angle %= DOUBLE_PI
def keys_control(self):
sin_a = math.sin(self.angle)
cos_a = math.cos(self.angle)
keys = pygame.key.get_pressed()
if keys[pygame.K_ESCAPE]:
exit()
if keys[pygame.K_w]:
dx = player_speed * cos_a
dy = player_speed * sin_a
self.detect_collision(dx, dy)
if keys[pygame.K_s]:
dx = -player_speed * cos_a
dy = -player_speed * sin_a
self.detect_collision(dx, dy)
if keys[pygame.K_a]:
dx = player_speed * sin_a
dy = -player_speed * cos_a
self.detect_collision(dx, dy)
if keys[pygame.K_d]:
dx = -player_speed * sin_a
dy = player_speed * cos_a
self.detect_collision(dx, dy)
if keys[pygame.K_LEFT]:
self.angle -= 0.02
if keys[pygame.K_RIGHT]:
self.angle += 0.02
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit()
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1 and not self.shot:
self.shot = True
def mouse_control(self):
if pygame.mouse.get_focused():
difference = pygame.mouse.get_pos()[0] - HALF_WIDTH
pygame.mouse.set_pos((HALF_WIDTH, HALF_HEIGHT))
self.angle += difference * self.sensitivity

76
ray_casting.py 100644
View File

@ -0,0 +1,76 @@
import pygame
from settings import *
from map import world_map, WORLD_WIDTH, WORLD_HEIGHT
from numba import njit
@njit(fastmath=True, cache=True)
def mapping(a, b):
return int(a // TILE) * TILE, int(b // TILE) * TILE
@njit(fastmath=True, cache=True)
def ray_casting(player_pos, player_angle, world_map):
casted_walls = []
ox, oy = player_pos
texture_v, texture_h = 1, 1
xm, ym = mapping(ox, oy)
cur_angle = player_angle - HALF_FOV
for ray in range(NUM_RAYS):
sin_a = math.sin(cur_angle)
sin_a = sin_a if sin_a else 0.000001
cos_a = math.cos(cur_angle)
cos_a = cos_a if cos_a else 0.000001
# verticals
x, dx = (xm + TILE, 1) if cos_a >= 0 else (xm, -1)
for i in range(0, WORLD_WIDTH, TILE):
depth_v = (x - ox) / cos_a
yv = oy + depth_v * sin_a
tile_v = mapping(x + dx, yv)
if tile_v in world_map:
texture_v = world_map[tile_v]
break
x += dx * TILE
# horizontals
y, dy = (ym + TILE, 1) if sin_a >= 0 else (ym, -1)
for i in range(0, WORLD_HEIGHT, TILE):
depth_h = (y - oy) / sin_a
xh = ox + depth_h * cos_a
tile_h = mapping(xh, y + dy)
if tile_h in world_map:
texture_h = world_map[tile_h]
break
y += dy * TILE
# projection
depth, offset, texture = (depth_v, yv, texture_v) if depth_v < depth_h else (depth_h, xh, texture_h)
offset = int(offset) % TILE
depth *= math.cos(player_angle - cur_angle)
depth = max(depth, 0.00001)
proj_height = int(PROJ_COEFF / depth)
casted_walls.append((depth, offset, proj_height, texture))
cur_angle += DELTA_ANGLE
return casted_walls
def ray_casting_walls(player, textures):
casted_walls = ray_casting(player.pos, player.angle, world_map)
wall_shot = casted_walls[CENTER_RAY][0], casted_walls[CENTER_RAY][2]
walls = []
for ray, casted_values in enumerate(casted_walls):
depth, offset, proj_height, texture = casted_values
if proj_height > HEIGHT:
coeff = proj_height / HEIGHT
texture_height = TEXTURE_HEIGHT / coeff
wall_column = textures[texture].subsurface(offset * TEXTURE_SCALE,
HALF_TEXTURE_HEIGHT - texture_height // 2,
TEXTURE_SCALE, texture_height)
wall_column = pygame.transform.scale(wall_column, (SCALE, HEIGHT))
wall_pos = (ray * SCALE, 0)
else:
wall_column = textures[texture].subsurface(offset * TEXTURE_SCALE, 0, TEXTURE_SCALE, TEXTURE_HEIGHT)
wall_column = pygame.transform.scale(wall_column, (SCALE, proj_height))
wall_pos = (ray * SCALE, HALF_HEIGHT - proj_height // 2)
walls.append((depth, wall_column, wall_pos))
return walls, wall_shot

BIN
requirements.txt 100644

Binary file not shown.

61
settings.py 100644
View File

@ -0,0 +1,61 @@
import math
# game settings
WIDTH = 1920
HEIGHT = 1080
HALF_WIDTH = WIDTH // 2
HALF_HEIGHT = HEIGHT // 2
PENTA_HEIGHT = 5 * HEIGHT
DOUBLE_HEIGHT = 2 * HEIGHT
FPS = 60
TILE = 100
FPS_POS = (WIDTH - 65, 5)
# minimap settings
MINIMAP_SCALE = 5
MINIMAP_RES = (WIDTH // MINIMAP_SCALE, HEIGHT // MINIMAP_SCALE)
MAP_SCALE = 1.25 * MINIMAP_SCALE # 1 -> 12 x 8, 2 -> 24 x 16, 3 -> 36 x 24
MAP_TILE = TILE // MAP_SCALE
MAP_POS = (0, HEIGHT - HEIGHT // MINIMAP_SCALE)
# ray casting settings
FOV = math.pi / 3
HALF_FOV = FOV / 2
NUM_RAYS = 300
MAX_DEPTH = 800
DELTA_ANGLE = FOV / NUM_RAYS
DIST = NUM_RAYS / (2 * math.tan(HALF_FOV))
PROJ_COEFF = 3 * DIST * TILE
SCALE = WIDTH // NUM_RAYS
# sprite settings
DOUBLE_PI = math.pi * 2
CENTER_RAY = NUM_RAYS // 2 - 1
FAKE_RAYS = 100
FAKE_RAYS_RANGE = NUM_RAYS - 1 + 2 * FAKE_RAYS
# texture settings (1200 x 1200)
TEXTURE_WIDTH = 1200
TEXTURE_HEIGHT = 1200
HALF_TEXTURE_HEIGHT = TEXTURE_HEIGHT // 2
TEXTURE_SCALE = TEXTURE_WIDTH // TILE
# player settings
player_pos = (HALF_WIDTH // 4, HALF_HEIGHT - 50)
player_angle = 0
player_speed = 3
PLAYER_HEALTH = 100
# colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (220, 0, 0)
GREEN = (0, 80, 0)
BLUE = (0, 0, 255)
DARKGRAY = (40, 40, 40)
PURPLE = (120, 0, 120)
SKYBLUE = (0, 186, 255)
YELLOW = (220, 220, 0)
SANDY = (244, 164, 96)
DARKBROWN = (97, 61, 25)
DARKORANGE = (255, 140, 0)

268
sprite_objects.py 100644
View File

@ -0,0 +1,268 @@
import pygame
from settings import *
from collections import deque
from ray_casting import mapping
from numba.core import types
from numba.typed import Dict
from numba import int32
class Sprites:
def __init__(self):
self.sprite_parameters = {
'sprite_barrel': {
'sprite': pygame.image.load('sprites/barrel/base/0.png').convert_alpha(),
'viewing_angles': None,
'shift': 1.8,
'scale': (0.4, 0.4),
'side': 30,
'damage': 0,
'animation': deque(
[pygame.image.load(f'sprites/barrel/anim/{i}.png').convert_alpha() for i in range(12)]),
'death_animation': deque([pygame.image.load(f'sprites/barrel/death/{i}.png')
.convert_alpha() for i in range(4)]),
'is_dead': None,
'dead_shift': 2.6,
'animation_dist': 800,
'animation_speed': 10,
'blocked': True,
'flag': 'decor',
'obj_action': []
},
'sprite_pin': {
'sprite': pygame.image.load('sprites/pin/base/0.png').convert_alpha(),
'viewing_angles': None,
'shift': 0.6,
'scale': (0.6, 0.6),
'side': 30,
'damage': 0,
'animation': deque([pygame.image.load(f'sprites/pin/anim/{i}.png').convert_alpha() for i in range(8)]),
'death_animation': [],
'is_dead': 'immortal',
'dead_shift': None,
'animation_dist': 800,
'animation_speed': 10,
'blocked': True,
'flag': 'decor',
'obj_action': []
},
'sprite_flame': {
'sprite': pygame.image.load('sprites/flame/base/0.png').convert_alpha(),
'viewing_angles': None,
'shift': 0.7,
'scale': (0.6, 0.6),
'side': 30,
'damage': 0,
'animation': deque(
[pygame.image.load(f'sprites/flame/anim/{i}.png').convert_alpha() for i in range(16)]),
'death_animation': [],
'is_dead': 'immortal',
'dead_shift': 1.8,
'animation_dist': 1800,
'animation_speed': 5,
'blocked': None,
'flag': 'decor',
'obj_action': []
},
'npc_devil': {
'sprite': [pygame.image.load(f'sprites/devil/base/{i}.png').convert_alpha() for i in range(8)],
'viewing_angles': True,
'shift': 0.0,
'scale': (1.1, 1.1),
'side': 50,
'damage': 2,
'animation': [],
'death_animation': deque([pygame.image.load(f'sprites/devil/death/{i}.png')
.convert_alpha() for i in range(6)]),
'is_dead': None,
'dead_shift': 0.6,
'animation_dist': None,
'animation_speed': 10,
'blocked': True,
'flag': 'npc',
'obj_action': deque(
[pygame.image.load(f'sprites/devil/anim/{i}.png').convert_alpha() for i in range(9)]),
},
'npc_soldier0': {
'sprite': [pygame.image.load(f'sprites/npc/soldier0/base/{i}.png').convert_alpha() for i in range(8)],
'viewing_angles': True,
'shift': 0.8,
'scale': (0.4, 0.6),
'side': 30,
'damage': 2,
'animation': [],
'death_animation': deque([pygame.image.load(f'sprites/npc/soldier0/death/{i}.png')
.convert_alpha() for i in range(10)]),
'is_dead': None,
'dead_shift': 1.7,
'animation_dist': None,
'animation_speed': 6,
'blocked': True,
'flag': 'npc',
'obj_action': deque([pygame.image.load(f'sprites/npc/soldier0/action/{i}.png')
.convert_alpha() for i in range(4)])
},
}
self.list_of_objects = [
SpriteObject(self.sprite_parameters['sprite_barrel'], (7.1, 2.1)),
SpriteObject(self.sprite_parameters['sprite_barrel'], (5.9, 2.1)),
SpriteObject(self.sprite_parameters['sprite_pin'], (8.7, 2.5)),
SpriteObject(self.sprite_parameters['npc_devil'], (7, 4)),
SpriteObject(self.sprite_parameters['sprite_flame'], (8.6, 5.6)),
SpriteObject(self.sprite_parameters['npc_soldier0'], (2.5, 1.5)),
SpriteObject(self.sprite_parameters['npc_soldier0'], (5.51, 1.5)),
SpriteObject(self.sprite_parameters['npc_soldier0'], (6.61, 2.92)),
SpriteObject(self.sprite_parameters['npc_soldier0'], (7.68, 1.47)),
SpriteObject(self.sprite_parameters['npc_soldier0'], (8.75, 3.65)),
SpriteObject(self.sprite_parameters['npc_soldier0'], (1.27, 11.5)),
SpriteObject(self.sprite_parameters['npc_soldier0'], (1.26, 8.29)),
]
@property
def sprite_shot(self):
return min([obj.is_on_fire for obj in self.list_of_objects], default=(float('inf'), 0))
@property
def blocked_doors(self):
blocked_doors = Dict.empty(key_type=types.UniTuple(int32, 2), value_type=int32)
for obj in self.list_of_objects:
if obj.flag in {'door_h', 'door_v'} and obj.blocked:
i, j = mapping(obj.x, obj.y)
blocked_doors[(i, j)] = 0
return blocked_doors
class SpriteObject:
def __init__(self, parameters, pos):
self.object = parameters['sprite'].copy()
self.viewing_angles = parameters['viewing_angles']
self.shift = parameters['shift']
self.scale = parameters['scale']
self.animation = parameters['animation'].copy()
# ---------------------
self.death_animation = parameters['death_animation'].copy()
self.is_dead = parameters['is_dead']
self.dead_shift = parameters['dead_shift']
# ---------------------
self.animation_dist = parameters['animation_dist']
self.animation_speed = parameters['animation_speed']
self.blocked = parameters['blocked']
self.flag = parameters['flag']
self.obj_action = parameters['obj_action'].copy()
self.x, self.y = pos[0] * TILE, pos[1] * TILE
self.side = parameters['side']
self.damage = parameters['damage']
self.dead_animation_count = 0
self.animation_count = 0
self.npc_action_trigger = False
self.door_open_trigger = False
self.door_prev_pos = self.y if self.flag == 'door_h' else self.x
self.delete = False
if self.viewing_angles:
if len(self.object) == 8:
self.sprite_angles = [frozenset(range(338, 361)) | frozenset(range(0, 23))] + \
[frozenset(range(i, i + 45)) for i in range(23, 338, 45)]
else:
self.sprite_angles = [frozenset(range(348, 361)) | frozenset(range(0, 11))] + \
[frozenset(range(i, i + 23)) for i in range(11, 348, 23)]
self.sprite_positions = {angle: pos for angle, pos in zip(self.sprite_angles, self.object)}
@property
def is_on_fire(self):
if CENTER_RAY - self.side // 2 < self.current_ray < CENTER_RAY + self.side // 2 and self.blocked:
return self.distance_to_sprite, self.proj_height
return float('inf'), None
def sprite_shot(self):
return min([obj.is_on_fire for obj in self.list_of_objects], default=(float('inf'), 0))
@property
def pos(self):
return self.x - self.side // 2, self.y - self.side // 2
def object_locate(self, player):
dx, dy = self.x - player.x, self.y - player.y
self.distance_to_sprite = math.sqrt(dx ** 2 + dy ** 2)
self.theta = math.atan2(dy, dx)
gamma = self.theta - player.angle
if dx > 0 and 180 <= math.degrees(player.angle) <= 360 or dx < 0 and dy < 0:
gamma += DOUBLE_PI
self.theta -= 1.4 * gamma
delta_rays = int(gamma / DELTA_ANGLE)
self.current_ray = CENTER_RAY + delta_rays
if self.flag not in {'door_h', 'door_v'}:
self.distance_to_sprite *= math.cos(HALF_FOV - self.current_ray * DELTA_ANGLE)
fake_ray = self.current_ray + FAKE_RAYS
if 0 <= fake_ray <= FAKE_RAYS_RANGE and self.distance_to_sprite > 30:
self.proj_height = min(int(PROJ_COEFF / self.distance_to_sprite),
DOUBLE_HEIGHT if self.flag not in {'door_h', 'door_v'} else HEIGHT)
sprite_width = int(self.proj_height * self.scale[0])
sprite_height = int(self.proj_height * self.scale[1])
half_sprite_width = sprite_width // 2
half_sprite_height = sprite_height // 2
shift = half_sprite_height * self.shift
# logic for doors, npc, decor
if self.is_dead and self.is_dead != 'immortal':
sprite_object = self.dead_animation()
shift = half_sprite_height * self.dead_shift
sprite_height = int(sprite_height / 1.3)
elif self.npc_action_trigger:
sprite_object = self.npc_in_action()
else:
self.object = self.visible_sprite()
sprite_object = self.sprite_animation()
# sprite scale and pos
sprite_pos = (self.current_ray * SCALE - half_sprite_width, HALF_HEIGHT - half_sprite_height + shift)
sprite = pygame.transform.scale(sprite_object, (sprite_width, sprite_height))
return (self.distance_to_sprite, sprite, sprite_pos)
else:
return (False,)
def sprite_animation(self):
if self.animation and self.distance_to_sprite < self.animation_dist:
sprite_object = self.animation[0]
if self.animation_count < self.animation_speed:
self.animation_count += 1
else:
self.animation.rotate()
self.animation_count = 0
return sprite_object
return self.object
def visible_sprite(self):
if self.viewing_angles:
if self.theta < 0:
self.theta += DOUBLE_PI
self.theta = 360 - int(math.degrees(self.theta))
for angles in self.sprite_angles:
if self.theta in angles:
return self.sprite_positions[angles]
return self.object
def dead_animation(self):
if len(self.death_animation):
if self.dead_animation_count < self.animation_speed:
self.dead_sprite = self.death_animation[0]
self.dead_animation_count += 1
else:
self.dead_sprite = self.death_animation.popleft()
self.dead_animation_count = 0
return self.dead_sprite
def npc_in_action(self):
sprite_object = self.obj_action[0]
if self.animation_count < self.animation_speed:
self.animation_count += 1
else:
self.obj_action.rotate()
self.animation_count = 0
return sprite_object