sword attack and take damage animations

This commit is contained in:
frank 2022-02-27 00:14:51 -05:00
parent 5cf548fc2e
commit ad0c68aedb
2 changed files with 240 additions and 59 deletions

297
NS.py
View File

@ -35,8 +35,7 @@ from lib.pgfw.pgfw.GameChild import GameChild
from lib.pgfw.pgfw.Sprite import Sprite, RainbowSprite
from lib.pgfw.pgfw.Animation import Animation
from lib.pgfw.pgfw.extension import (
get_step, get_step_relative, get_delta, reflect_angle,
render_box, get_hsla_color, get_hue_shifted_surface,
get_step, get_step_relative, get_delta, reflect_angle, get_distance, render_box, get_hsla_color, get_hue_shifted_surface,
get_color_swapped_surface, load_frames, fill_colorkey
)
from lib.pgfw.pgfw.gfx_extension import aa_filled_polygon
@ -1296,8 +1295,6 @@ class Platform(GameChild):
for light in self.lights:
if light.glowing():
self.get_display_surface().blit(self.glow_masks[light.position][light.glow_index], self.view.location, None, BLEND_RGBA_ADD)
# for light in self.lights:
# light.draw_glow()
class Light(Animation):
@ -1580,8 +1577,14 @@ class Timer(Meter):
class Life(Meter):
"""
This class stores the state of the player's HP
"""
def __init__(self, parent):
"""
Initialize the Meter super class and graphics
"""
Meter.__init__(self, parent)
dsr = self.get_display_surface().get_rect()
background = load(self.get_resource("HUD_health.png")).convert()
@ -1590,8 +1593,13 @@ class Life(Meter):
self.setup(background, rect, 70, (255, 0, 0), 3, "scrapeIcons/scrapeIcons_03.png")
def decrease(self):
"""
Remove one health point. Set the current sword to attacking the chameleon. If this meter is depleted, remove a life
and trigger the boss's battle finish method.
"""
self.get_audio().play_sfx("hurt")
self.change(-1)
self.get_game().boss.sword.attack(player=True)
if self.amount <= 0:
self.amount = 0
self.parent.boys.change(-1)
@ -1631,26 +1639,42 @@ class Boss(Animation):
self.kool_man.load_from_path("Kool_man_waah.png", True)
self.spoopy = Sprite(self)
self.spoopy.load_from_path("Spoopy.png", True)
# Set up alien sprite with boil and hurt animations
self.visitor = Sprite(self, 42)
self.visitor.add_frameset(name="hurt", switch=True)
self.visitor.load_from_path("alienAnimations/alienHit", True)
self.visitor.add_frameset(name="normal", switch=True)
self.visitor.load_from_path("alienAnimations/alienBoil", True)
for sprite in self.kool_man, self.visitor, self.spoopy:
sprite.location.topleft = 207, 10
self.health = Health(self)
self.sword = Sword(self)
self.register(self.brandish, self.cancel_flash, self.show_introduction_dialogue,
self.show_end_dialogue, self.end_dialogue)
self.register(self.brandish, self.cancel_flash, self.show_introduction_dialogue, self.show_end_dialogue, self.end_dialogue,
self.end_player_damage, self.end_hit_animation)
self.register(self.flash_player_damage, interval=100)
self.kool_man.add_frameset([0], name="normal", switch=True)
# Set alien's normal frameset to an idle animation
self.visitor.add_frameset(list(range(0, len(self.visitor.frames))), name="normal", switch=True)
self.spoopy.add_frameset([0], name="normal", switch=True)
self.kool_man_avatar = load(self.get_resource("Kool_man_avatar.png")).convert()
self.visitor_avatar = load(self.get_resource("Visitor_avatar.png")).convert()
self.spoopy_avatar = load(self.get_resource("Spoopy_avatar.png")).convert()
self.advance_prompt = AdvancePrompt(self)
self.backgrounds = [Sprite(self), Sprite(self), Sprite(self)]
self.backgrounds[0].load_from_path(self.get_resource("bg/bg001.png"))
self.backgrounds[1].load_from_path(self.get_resource("bg/bg002.png"))
self.backgrounds[2].load_from_path(self.get_resource("bg/bg003.png"))
# Add background graphics and generate screen effects
for ii, background in enumerate(self.backgrounds):
background.add_frameset(name="normal", switch=True)
background.load_from_path(f"bg/bg00{ii + 1}.png")
background.add_frameset(name="inverted", switch=True)
frame = pygame.Surface(background.frames[0].get_size())
frame.fill((255, 255, 255))
frame.blit(background.frames[0], (0, 0), None, pygame.BLEND_RGB_SUB)
background.add_frame(frame)
background.add_frameset(name="charging", switch=True)
frame = background.frames[0].copy()
mask = frame.copy()
mask.fill((80, 80, 80))
frame.blit(mask, (0, 0), None, pygame.BLEND_RGB_SUB)
background.add_frame(frame)
background.set_frameset("normal")
self.countdown = Countdown(self)
# Set the alien's arm to its own sprite
self.alien_arm = Sprite(self, 42)
@ -1710,8 +1734,8 @@ class Boss(Animation):
def cancel_flash(self):
if self.level_index == 0:
self.kool_man.set_frameset("normal")
elif self.level_index == 1:
self.visitor.set_frameset("normal")
# elif self.level_index == 1:
# self.visitor.set_frameset("normal")
elif self.level_index == 2:
self.spoopy.set_frameset("normal")
@ -1775,6 +1799,11 @@ class Boss(Animation):
self.brandish_complete = True
self.countdown.reset()
self.halt(self.end_dialogue)
self.halt(self.flash_player_damage)
self.halt(self.end_player_damage)
for background in self.backgrounds:
background.set_frameset("normal")
self.halt(self.end_hit_animation)
def deactivate(self):
self.active = False
@ -1953,6 +1982,10 @@ class Boss(Animation):
self.sword.reset()
self.sword.play(self.sword.brandish, play_once=True)
self.get_game().chemtrails.challenge()
self.backgrounds[self.level_index].set_frameset("charging")
# Set each boss to its normal frameset
# for boss in (self.kool_man, self.visitor, self.spoopy):
# boss.set_frameset("normal")
def choose_new_edge(self, edges):
while True:
@ -2026,8 +2059,8 @@ class Boss(Animation):
def damage(self):
if self.level_index == 0:
self.kool_man.set_frameset(0)
elif self.level_index == 1:
self.visitor.set_frameset(0)
# elif self.level_index == 1:
# self.visitor.set_frameset("normal")
elif self.level_index == 2:
self.spoopy.set_frameset(0)
@ -2038,6 +2071,37 @@ class Boss(Animation):
else:
self.get_game().wipe.start(self.transition_to_battle)
def start_player_damage(self):
"""
Launch the flash player damage effect and queue it to end after a certain amount of time
"""
self.play(self.flash_player_damage)
self.play(self.end_player_damage, play_once=True, delay=1500)
def flash_player_damage(self):
"""
Invert the screen to indicate player has taken damage.
"""
background = self.backgrounds[self.level_index]
if background.get_current_frameset().name == "normal":
background.set_frameset("inverted")
else:
background.set_frameset("normal")
def end_player_damage(self):
"""
Halt the flash player damage animation and return the background to normal
"""
self.halt(self.flash_player_damage)
self.backgrounds[self.level_index].set_frameset("normal")
def end_hit_animation(self):
"""
Return boss's animation to normal
"""
for boss in (self.kool_man, self.visitor, self.spoopy):
boss.set_frameset("normal")
def update(self):
if self.active:
self.backgrounds[self.level_index].update()
@ -2144,92 +2208,150 @@ class Countdown(GameChild):
class Sword(Animation):
"""
This class stores the graphics for the swords that appear as hovering sprites when the boss is attacking.
"""
SHIFT = 15
SPRITE_COUNT = 6
def __init__(self, parent):
"""
Allocate and populate lists of sword animation frames. For each boss, create sword and spinning sword animation frames. For each
animation, create six color versions, one for each orientation on the platform.
"""
Animation.__init__(self, parent)
swords = self.swords = []
#for root in "Sword_kool_man/", "Sword_visitor/", "Sword_spoopy/":
# These will be three dimensional lists: swords[boss][position][frame]
self.swords = []
self.spinning_swords = []
def rotate_sword(base, position):
"""
Rotate the sword based on the orientation of the board in this position
"""
if position == NS.N or position == NS.S:
rotated = rotate(base, 270)
elif position == NS.NW:
rotated = rotate(base, 45)
elif position == NS.NE:
rotated = rotate(base, 310)
else:
rotated = base
return rotated
def fill_sword(surface, position, colors):
"""
Blend the platform colors into the grayscale base image
"""
rect = surface.get_rect()
if position == NS.N or position == NS.S:
surface.fill(colors[0], (0, 0, rect.w / 2, rect.h), BLEND_RGBA_MIN)
surface.fill(colors[1], (rect.centerx, 0, rect.w / 2, rect.h), BLEND_RGBA_MIN)
else:
surface.fill(colors[0], (0, 0, rect.w, rect.h / 2), BLEND_RGBA_MIN)
surface.fill(colors[1], (0, rect.centery, rect.w, rect.h / 2), BLEND_RGBA_MIN)
# Open a folder of sword frames for each boss
for root in "Sword_kool_man/", "local/test/", "Sword_spoopy/":
swords.append([[], [], [], [], [], []])
# Create a list of lists of sword frames, each list of sword frames corresponds to an orientation on the platform
self.swords.append([[], [], [], [], [], []])
self.spinning_swords.append([[], [], [], [], [], []])
base_image_paths = sorted(iglob(join(self.get_resource(root), "*.png")))
# If effects are turned off, use a single frame
if not self.get_configuration("display", "effects"):
base_image_paths = [base_image_paths[0]]
# Create a square surface that can be used to blit the rotated sword centered so each frame is the same size
size = max(*load(self.get_resource(base_image_paths[0])).get_size())
background = pygame.Surface((size, size), pygame.SRCALPHA)
# Create spinning sword effect by rotating the first base frame image, one for each platform position
for position in range(6):
base = rotate_sword(load(self.get_resource(base_image_paths[0])).convert_alpha(), position)
# Blend the appropriate colors into the base image
fill_sword(base, position, self.get_game().platform.get_color_pair_from_edge(position))
# Create a frame for each angle and store it in the list
for angle in range(0, 360, 36):
rotated = rotate(base, angle)
frame = background.copy()
rect = rotated.get_rect()
rect.center = frame.get_rect().center
frame.blit(rotated, rect)
self.spinning_swords[-1][position].append(frame)
# Create frames for each platform orientation by rotating the base frame images and blending colors over them
for frame_index, path in enumerate(base_image_paths):
base = load(self.get_resource(path)).convert_alpha()
# Iterate over all six orientation possibilities
for position in range(6):
if position == NS.N or position == NS.S:
rotated = rotate(base, 270)
elif position == NS.NW:
rotated = rotate(base, 45)
elif position == NS.NE:
rotated = rotate(base, 310)
else:
rotated = base
rotated = rotate_sword(base, position)
surface = rotated.copy()
colors = self.get_game().platform.get_color_pair_from_edge(position)
color_a = Color(colors[0].r, colors[0].g, colors[0].b, 255)
color_b = Color(colors[1].r, colors[1].g, colors[1].b, 255)
# edit lightness to create glowing effect
# Edit lightness to create glowing effect
for color in (color_a, color_b):
h, s, l, a = color.hsla
l = 30 + int(abs((frame_index % 10) - 5) / 5 * 60)
color.hsla = h, s, l, a
rect = surface.get_rect()
if position == NS.N or position == NS.S:
surface.fill(color_a, (0, 0, rect.w / 2, rect.h), BLEND_RGBA_MIN)
surface.fill(color_b, (rect.centerx, 0, rect.w / 2, rect.h), BLEND_RGBA_MIN)
else:
surface.fill(color_a, (0, 0, rect.w, rect.h / 2), BLEND_RGBA_MIN)
surface.fill(color_b, (0, rect.centery, rect.w, rect.h / 2), BLEND_RGBA_MIN)
swords[-1][position].append(surface)
masks = self.masks = []
for alpha in range(16, 255, 16):
surface = Surface((300, 300), SRCALPHA)
surface.fill((255, 255, 255, alpha))
masks.append(surface)
fill_sword(surface, position, (color_a, color_b))
self.swords[-1][position].append(surface)
self.register(self.brandish, self.lower, self.swab)
def reset(self):
"""
Halt animations, clear sprites
"""
self.halt(self.brandish)
self.halt(self.lower)
self.halt(self.swab)
self.next_index = 0
self.sprites = []
self.active_sprite_index = 0
self.attacking_player = False
def brandish(self):
"""
Get the next sword position to brandish from `self.parent`, create a sprite with a regular rotating frameset and spinning attack
frameset, place it around a rectangle in the center of the screen, and store it in a list.
"""
level_index = self.parent.level_index
position = self.parent.unbrandished.pop(0)
offset = -self.SHIFT
for ii, queued in enumerate(self.parent.queue):
offset += self.SHIFT * (queued == position)
if len(self.parent.unbrandished) == len(self.parent.queue) - ii - 1:
break
dsr = self.get_display_surface().get_rect()
sprite = Sprite(self)
for frame in self.swords[level_index][position]:
# Add frames from storage for regular and attacking animations
for frame in self.swords[level_index][position] + self.spinning_swords[level_index][position]:
sprite.add_frame(frame)
sprite.add_frameset(list(range(len(self.swords[level_index][position]), len(sprite.frames))), name="attack")
# Add an explosion effect frameset
sprite.add_frameset(name="explode", switch=True, framerate=70)
surface = pygame.Surface((200, 200), pygame.SRCALPHA)
thickness = 6
color = pygame.Color(0, 0, 0)
for radius in range(6, 100, 4):
frame = surface.copy()
ratio = float(radius - 6) / (100 - 6)
color.hsla = 60 * (1 - ratio), 100, 50, 100
pygame.draw.circle(frame, color, (100, 100), radius, max(1, int(thickness)))
thickness -= .2
sprite.add_frame(frame)
sprite.add_frameset(list(range(0, len(self.swords[level_index][position]))), name="normal", switch=True)
# Place on screen around an invisible rectangle in the center
if position in (NS.W, NS.E):
sprite.location.centery = dsr.centery - 80 + offset
sprite.location.centery = dsr.centery - 80
if position == NS.W:
sprite.location.centerx = dsr.centerx - 100 - offset
sprite.location.centerx = dsr.centerx - 100
else:
sprite.location.centerx = dsr.centerx + 100 - offset
sprite.location.centerx = dsr.centerx + 100
elif position in (NS.N, NS.S):
sprite.location.centerx = dsr.centerx - offset
sprite.location.centerx = dsr.centerx
if position == NS.N:
sprite.location.centery = dsr.centery - 150 + offset
sprite.location.centery = dsr.centery - 150
else:
sprite.location.centery = dsr.centery + 20 + offset
sprite.location.centery = dsr.centery + 20
else:
sprite.location.center = dsr.centerx - offset, dsr.centery - 80
sprite.location.center = dsr.centerx, dsr.centery - 80
self.sprites.append(sprite)
self.get_audio().play_sfx("brandish")
# Set brandish to complete on a delay
self.play(self.lower, delay=400, play_once=True)
# Brandish more swords
if len(self.parent.unbrandished) > 0:
self.play(self.brandish, delay=self.get_configuration("time", "sword-delay"), play_once=True)
# Trigger boss's sword swab animation on a delay
if level_index == 1:
self.parent.alien_arm.unhide()
self.parent.alien_arm.set_frameset(str(position))
@ -2237,24 +2359,83 @@ class Sword(Animation):
self.play(self.swab, delay=self.get_configuration("time", "sword-delay") - 42 * 4, play_once=True, position=position)
def swab(self, position):
"""
Activate boss's sword swab animation
"""
if self.parent.level_index == 1:
self.parent.alien_arm.set_frameset(f"{position}_{self.parent.unbrandished[0]}")
def lower(self):
"""
Set brandish to complete.
"""
if len(self.parent.unbrandished) == 0:
self.parent.brandish_complete = True
self.parent.backgrounds[self.parent.level_index].set_frameset("normal")
if self.parent.level_index == 1:
self.parent.alien_arm.hide()
def block(self):
if len(self.sprites):
self.sprites.pop(0)
"""
Successfully block a sword move, setting the sprite to attacking and moving the active index.
"""
if self.sprites:
self.attack(player=False)
self.active_sprite_index += 1
def attack(self, player):
"""
Set the currently active sprite to its attacking animation.
@param player boolean that sets whether the attack is toward the player or boss
"""
center_save = self.sprites[self.active_sprite_index].location.center
self.sprites[self.active_sprite_index].set_frameset("attack")
self.sprites[self.active_sprite_index].location.center = center_save
self.attacking_player = player
def update(self):
"""
Draw previously blocked swords and the boss's current move sword.
"""
Animation.update(self)
# only draw the current sword in the queue
if self.sprites:
self.sprites[0].update()
for ii, sprite in enumerate(self.sprites[:self.active_sprite_index + 1]):
if sprite.get_current_frameset().name == "attack" and not sprite.is_hidden():
if self.attacking_player and ii == self.active_sprite_index:
scale = 1.1
end = self.get_game().platform.view.location.center
else:
scale = 0.95
end = self.get_game().boss.visitor.location.center
for frame_index in sprite.get_current_frameset().order:
width, height = sprite.frames[frame_index].get_size()
if width < 800 and height < 800:
scaled_width, scaled_height = int(width * scale), int(height * scale)
sprite.frames[frame_index] = pygame.transform.scale(sprite.frames[frame_index], (scaled_width, scaled_height))
if width >= 800 or width < 75 or height >= 800 or height < 75:
if self.attacking_player and ii == self.active_sprite_index:
sprite.hide()
self.get_game().boss.start_player_damage()
else:
center_save = sprite.location.center
sprite.set_frameset("explode")
sprite.location.center = center_save
if self.parent.level_index == 1:
if self.parent.is_playing(self.parent.end_hit_animation, include_delay=True):
self.parent.halt(self.parent.end_hit_animation)
self.parent.visitor.set_frameset("hurt")
self.parent.play(self.parent.end_hit_animation, play_once=True, delay=500)
else:
center_save = sprite.location.center
sprite.get_current_frameset().measure_rect()
sprite.update_location_size()
sprite.location.center = center_save
# sprite.move(*get_step(sprite.location.center, end, 4))
elif sprite.get_current_frameset().name == "explode" and not sprite.is_hidden():
if sprite.get_current_frameset().get_current_id() == sprite.get_current_frameset().order[-1]:
sprite.hide()
sprite.update()
class Health(Meter):

@ -1 +1 @@
Subproject commit 6693ca45140aa67df7a6b0622a98d8d5b59079a3
Subproject commit f5de024ef3627a20191b0a36308ac8ad8a4e6e6c