1
0
Fork 0
mirror of https://github.com/EDeev/pixel_gamble.git synced 2026-10-07 23:49:29 +03:00
pixel_gamble/game/pixelgamble/boss.py
Egor Deev 4ecb9257c8 Pixel Gamble 2.0: законченная игра на ресурсах Ninja Adventure
- 10 волн, 5 видов врагов со своим поведением и босс в три фазы
- меч, копьё и посох, 11 улучшений между волнами, опыт и уровни
- меню, пауза, настройки, итоги партии и рекорд, русский и английский
- мир 480×270 с масштабированием, интерфейс в 960×540, скорости от dt
- асинхронный цикл: та же игра в браузере через pygbag
- тесты без окна, автоигрок, CI, релизы exe/Linux, GitHub Pages
- README на двух языках; исходная версия — релиз v1.0.0
2026-10-07 12:29:41 +00:00

168 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Босс финальной волны — Красный самурай. Три фазы по уровню здоровья."""
import math
import random
import pygame
from . import assets
from .enemies import Enemy
from .entity import Entity, Vector
BOSS_HP = 2600
# в кадрах 96×96 ноги нарисованы выше нижнего края, чем в кадрах 96×48 — опускаем их до тени
FEET = {"attack": 15, "charge": 19}
class Boss(Enemy):
boss = True
heavy = True
def __init__(self, pos, hp_mult=1.0):
Entity.__init__(self, pos, 30, 16) # минуя Enemy.__init__: босса нет в таблице KINDS
self.kind = "boss"
self.max_hp = self.hp = BOSS_HP * hp_mult
self.speed = 34
self.contact = 2
self.xp, self.score = 0, 3000
self.state, self.timer = "spawn", 1.4
self.cooldown = 1.5
self.dash_cooldown = 4.0
self.summon_cooldown = 6.0
self.burst_cooldown = 5.0
self.dir = Vector()
self.side = "right"
self.phase = 1
self.slash_hit = False
self.load_frames()
def load_frames(self):
self.frames = {
"walk": assets.strip("chars/boss_walk.png", 96),
"idle": assets.strip("chars/boss_idle.png", 96),
"hit": assets.strip("chars/boss_hit.png", 96),
"attack_left": assets.strip("chars/boss_attack_l.png", 96),
"attack_right": assets.strip("chars/boss_attack_r.png", 96),
"charge_left": assets.strip("chars/boss_charge_l.png", 96),
"charge_right": assets.strip("chars/boss_charge_r.png", 96),
}
@property
def active(self):
return self.state not in ("spawn", "roar")
def hurt(self, amount, source_pos, knock):
if not self.active:
return False
self.hp -= amount
self.flash = 0.08
new_phase = 1 if self.hp > self.max_hp * 0.6 else 2 if self.hp > self.max_hp * 0.3 else 3
if new_phase > self.phase and self.hp > 0:
self.phase = new_phase
self.state, self.timer = "roar", 1.0
return True
def slash_box(self):
box = pygame.Rect(0, 0, 58, 44)
if self.side == "right":
box.midleft = (self.hitbox.centerx - 6, self.hitbox.centery - 12)
else:
box.midright = (self.hitbox.centerx + 6, self.hitbox.centery - 12)
return box
def burst(self, game, count):
start = random.random() * math.tau
for i in range(count):
angle = start + math.tau * i / count
game.enemy_shot(self.pos + Vector(0, -10), Vector(math.cos(angle), math.sin(angle)) * 95, 1)
game.audio.play("enemy_shoot")
def think(self, dt, game, obstacles):
player = game.player
dist = self.distance_to(player)
to_player = self.direction_to(player)
self.dash_cooldown -= dt
self.summon_cooldown -= dt
self.burst_cooldown -= dt
if to_player.x:
facing_side = "right" if to_player.x > 0 else "left"
else:
facing_side = self.side
if self.state == "roar":
self.timer -= dt
game.shake.add(1.5)
if self.timer <= 0:
self.state = "move"
game.audio.play("boss_appear")
return
if self.state == "move":
self.side = facing_side
speed = self.speed * (1.3 if self.phase == 3 else 1.0)
if dist > 34:
self.chase(dt, game, obstacles, speed=speed)
if dist < 52 and self.cooldown <= 0:
self.state, self.timer = "slash_windup", 0.45
elif self.phase >= 2 and dist > 90 and self.dash_cooldown <= 0:
self.state, self.timer = "dash_windup", 0.6
self.dir = Vector(to_player)
elif self.phase == 3 and self.summon_cooldown <= 0:
self.summon_cooldown = 9.0
game.summon(["ninja", "bat", "bat"], self.pos)
elif self.phase == 3 and self.burst_cooldown <= 0:
self.burst_cooldown = 5.0
self.burst(game, 12)
elif self.state == "slash_windup":
self.timer -= dt
if self.timer <= 0:
self.state, self.timer, self.slash_hit = "slash", 0.36, False
game.audio.play("boss_slash")
elif self.state == "slash":
self.timer -= dt
if not self.slash_hit and self.timer < 0.28 and self.slash_box().colliderect(player.hitbox):
self.slash_hit = game.damage_player(3, self.pos)
if self.timer <= 0:
self.state, self.cooldown = "move", 1.3 if self.phase < 3 else 0.9
elif self.state == "dash_windup":
self.timer -= dt
self.side = "right" if self.dir.x >= 0 else "left"
if self.timer <= 0:
self.state, self.timer = "dash", 0.55
game.audio.play("boss_stomp")
elif self.state == "dash":
self.timer -= dt
before = Vector(self.pos)
self.move(self.dir * 260 * dt, obstacles)
if self.timer <= 0 or self.pos.distance_squared_to(before) < 0.5:
self.state, self.dash_cooldown = "move", 4.5 if self.phase == 2 else 3.2
game.shake.add(3)
self.burst(game, 8)
def animate(self, dt):
self.anim += dt
if self.state == "spawn":
self.image = assets.white(self.frames["idle"][0]) if int(self.timer * 10) % 2 else self.frames["idle"][0]
self.rect = self.image.get_rect(midbottom=(self.hitbox.centerx, self.hitbox.bottom + 4))
return
if self.state == "roar":
frames = self.frames["hit"]
elif self.state in ("slash_windup", "dash_windup"):
frames = self.frames[f"charge_{self.side}"]
image = frames[min(len(frames) - 1, int(self.anim * 9) % len(frames))]
self.place_image(image, FEET["charge"])
if self.state == "dash_windup" and int(self.timer * 20) % 2:
self.image = assets.white(image)
return
elif self.state == "slash":
frames = self.frames[f"attack_{self.side}"]
image = frames[min(len(frames) - 1, int((0.36 - self.timer) / 0.36 * len(frames)))]
self.place_image(image, FEET["attack"])
return
elif self.state == "dash":
frames = self.frames["walk"]
else:
frames = self.frames["walk"] if self.dir.length_squared() else self.frames["idle"]
speed = 18 if self.state == "dash" else 9
image = frames[int(self.anim * speed) % len(frames)]
self.place_image(image, 4)