mirror of
https://github.com/EDeev/pixel_gamble.git
synced 2026-10-07 23:49:29 +03:00
- 10 волн, 5 видов врагов со своим поведением и босс в три фазы - меч, копьё и посох, 11 улучшений между волнами, опыт и уровни - меню, пауза, настройки, итоги партии и рекорд, русский и английский - мир 480×270 с масштабированием, интерфейс в 960×540, скорости от dt - асинхронный цикл: та же игра в браузере через pygbag - тесты без окна, автоигрок, CI, релизы exe/Linux, GitHub Pages - README на двух языках; исходная версия — релиз v1.0.0
155 lines
5.6 KiB
Python
155 lines
5.6 KiB
Python
"""Игрок и его оружие."""
|
||
|
||
import pygame
|
||
|
||
from . import assets
|
||
from .entity import DIR_VECTORS, Entity, Vector, direction_name
|
||
from .settings import PLAYER_HEARTS, PLAYER_INVULNERABLE, PLAYER_SPEED
|
||
|
||
WEAPONS = {
|
||
# kind: arc — широкий взмах перед собой, thrust — длинный узкий укол насквозь, bolt — снаряд
|
||
"sword": {"kind": "arc", "damage": 12, "cooldown": 0.36, "reach": 20, "width": 34, "knock": 160},
|
||
"lance": {"kind": "thrust", "damage": 17, "cooldown": 0.52, "reach": 38, "width": 12, "knock": 120},
|
||
"wand": {"kind": "bolt", "damage": 9, "cooldown": 0.40, "speed": 240, "life": 0.85, "knock": 70},
|
||
}
|
||
WEAPON_ORDER = ("sword", "lance", "wand")
|
||
|
||
|
||
class Player(Entity):
|
||
heavy = False
|
||
|
||
def __init__(self, pos):
|
||
super().__init__(pos, 10, 8)
|
||
self.frames = assets.character("player.png")
|
||
self.max_hp = PLAYER_HEARTS * 4 # здоровье в четвертях сердечка
|
||
self.hp = self.max_hp
|
||
self.speed = PLAYER_SPEED
|
||
self.damage_mult = 1.0
|
||
self.cooldown_mult = 1.0
|
||
self.reach_mult = 1.0
|
||
self.knock_mult = 1.0
|
||
self.vampire = 0.0
|
||
self.multishot = False
|
||
self.weapons = ["sword"]
|
||
self.weapon = "sword"
|
||
self.cooldown = 0.0
|
||
self.attack_pose = 0.0
|
||
self.invulnerable = 0.0
|
||
self.aim = Vector(0, 1)
|
||
self.moving = False
|
||
self.level = 1
|
||
self.xp = 0
|
||
self.dead = False
|
||
|
||
# --- характеристики ---
|
||
def damage(self):
|
||
return WEAPONS[self.weapon]["damage"] * self.damage_mult
|
||
|
||
def heal(self, amount):
|
||
before = self.hp
|
||
self.hp = min(self.max_hp, self.hp + amount)
|
||
return self.hp > before
|
||
|
||
def hurt(self, amount, source_pos):
|
||
if self.invulnerable > 0 or self.dead:
|
||
return False
|
||
self.hp = max(0, self.hp - amount)
|
||
self.invulnerable = PLAYER_INVULNERABLE
|
||
self.flash = 0.12
|
||
away = self.pos - Vector(source_pos)
|
||
if away.length_squared() > 0:
|
||
self.knock = away.normalize() * 170
|
||
if self.hp == 0:
|
||
self.dead = True
|
||
return True
|
||
|
||
def switch(self, step=1, index=None):
|
||
if index is not None:
|
||
if index < len(self.weapons):
|
||
self.weapon = self.weapons[index]
|
||
return True
|
||
return False
|
||
if len(self.weapons) < 2:
|
||
return False
|
||
i = (self.weapons.index(self.weapon) + step) % len(self.weapons)
|
||
self.weapon = self.weapons[i]
|
||
return True
|
||
|
||
def unlock(self, weapon):
|
||
if weapon not in self.weapons:
|
||
self.weapons.append(weapon)
|
||
self.weapons.sort(key=WEAPON_ORDER.index)
|
||
self.weapon = weapon
|
||
|
||
# --- кадр ---
|
||
def update(self, dt, move, obstacles):
|
||
self.cooldown = max(0.0, self.cooldown - dt)
|
||
self.attack_pose = max(0.0, self.attack_pose - dt)
|
||
self.invulnerable = max(0.0, self.invulnerable - dt)
|
||
self.flash = max(0.0, self.flash - dt)
|
||
|
||
self.moving = move.length_squared() > 0
|
||
if self.moving:
|
||
move = move.normalize()
|
||
self.aim = Vector(move)
|
||
if self.attack_pose <= 0:
|
||
self.facing = direction_name(move)
|
||
speed = self.speed * (0.45 if self.attack_pose > 0 else 1.0)
|
||
self.move(move * speed * dt, obstacles)
|
||
self.apply_knock(dt, obstacles)
|
||
self.animate(dt)
|
||
|
||
def try_attack(self, target=None):
|
||
"""Начать атаку. target — точка в мире (курсор) или None (по направлению взгляда)."""
|
||
if self.cooldown > 0 or self.dead:
|
||
return None
|
||
weapon = WEAPONS[self.weapon]
|
||
if target is not None:
|
||
aim = Vector(target) - self.pos
|
||
if aim.length_squared() > 0:
|
||
self.aim = aim.normalize()
|
||
if weapon["kind"] == "bolt":
|
||
direction = Vector(self.aim)
|
||
else:
|
||
self.facing = direction_name(self.aim)
|
||
direction = DIR_VECTORS[self.facing]
|
||
self.facing = direction_name(direction)
|
||
self.cooldown = weapon["cooldown"] * self.cooldown_mult
|
||
self.attack_pose = 0.18
|
||
return direction
|
||
|
||
def melee_box(self, direction):
|
||
weapon = WEAPONS[self.weapon]
|
||
reach = weapon["reach"] * self.reach_mult
|
||
width = weapon["width"] * (self.reach_mult if weapon["kind"] == "arc" else 1.0)
|
||
center = self.pos + Vector(0, -4) + direction * (reach / 2 + 4)
|
||
if direction.x:
|
||
box = pygame.Rect(0, 0, reach, width)
|
||
else:
|
||
box = pygame.Rect(0, 0, width, reach)
|
||
box.center = (round(center.x), round(center.y))
|
||
return box
|
||
|
||
def animate(self, dt):
|
||
frames = self.frames[self.facing]
|
||
if self.attack_pose > 0:
|
||
image = frames["attack"][0]
|
||
elif self.moving:
|
||
self.anim = (self.anim + dt * 8) % 4
|
||
image = frames["walk"][int(self.anim)]
|
||
else:
|
||
image = frames["walk"][0]
|
||
self.place_image(image, 3)
|
||
if self.invulnerable > 0 and int(self.invulnerable * 20) % 2 and self.flash <= 0:
|
||
self.image = self.image.copy()
|
||
self.image.set_alpha(110)
|
||
|
||
def gain_xp(self, amount, need):
|
||
"""Возвращает число новых уровней."""
|
||
self.xp += amount
|
||
levels = 0
|
||
while self.xp >= need(self.level):
|
||
self.xp -= need(self.level)
|
||
self.level += 1
|
||
levels += 1
|
||
return levels
|