From ad0c68aedbdae03487fa5f0279cca5235bdc2ee0 Mon Sep 17 00:00:00 2001 From: frank <420@shampoo.ooo> Date: Sun, 27 Feb 2022 00:14:51 -0500 Subject: [PATCH] sword attack and take damage animations --- NS.py | 297 ++++++++++++++++++++++++++++++++++++++++++++----------- lib/pgfw | 2 +- 2 files changed, 240 insertions(+), 59 deletions(-) diff --git a/NS.py b/NS.py index 5ddfffa..938690c 100644 --- a/NS.py +++ b/NS.py @@ -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): diff --git a/lib/pgfw b/lib/pgfw index 6693ca4..f5de024 160000 --- a/lib/pgfw +++ b/lib/pgfw @@ -1 +1 @@ -Subproject commit 6693ca45140aa67df7a6b0622a98d8d5b59079a3 +Subproject commit f5de024ef3627a20191b0a36308ac8ad8a4e6e6c