Cocktail Frank
0909b97d6e
Fix the mod key check so that shift isn't erroneously checked for anymore.
3954 lines
172 KiB
Python
3954 lines
172 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# [SCRAPEBOARD] is an arcade game in development by [@diskmem] & [@snakesandrews].
|
|
#
|
|
# It requires custom hardware to play but can be tested in keyboard mode without
|
|
# the hardware. For more information on setting up and running the game, see
|
|
# README.md, or for the game in general, visit <https://scrape.nugget.fun/>.
|
|
#
|
|
# The code, assets, and hardware are released as open source. See <https://open.shampoo.ooo/scrape/scrapeboard>.
|
|
#
|
|
# This is the main file containing all the Pygame code.
|
|
|
|
import argparse, pathlib, operator, subprocess, sys, os, socket, select, time, random, datetime
|
|
|
|
# Auto-detect GPIO library
|
|
try:
|
|
import gpio
|
|
except ImportError:
|
|
pass
|
|
|
|
from math import pi
|
|
from copy import copy
|
|
from glob import iglob
|
|
from os.path import basename, join
|
|
from threading import Thread
|
|
from time import sleep
|
|
from PIL import Image
|
|
from collections import deque
|
|
from itertools import chain
|
|
|
|
import pygame
|
|
from pygame import Surface, mixer
|
|
from pygame.event import clear
|
|
from pygame.mixer import Sound
|
|
from pygame.image import load, fromstring
|
|
from pygame.transform import rotate, flip, scale, smoothscale
|
|
from pygame.time import get_ticks
|
|
from pygame.draw import aalines, lines
|
|
from pygame.gfxdraw import aapolygon, arc, polygon, aaellipse, ellipse, filled_ellipse, filled_circle
|
|
from pygame.locals import *
|
|
|
|
from lib.pgfw.pgfw.Game import Game
|
|
from lib.pgfw.pgfw.GameChild import GameChild
|
|
from lib.pgfw.pgfw.Sprite import Sprite, RainbowSprite, BlinkingSprite
|
|
from lib.pgfw.pgfw.Animation import Animation
|
|
from lib.pgfw.pgfw.Vector import Vector
|
|
from lib.pgfw.pgfw.extension import (
|
|
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, get_segments,
|
|
get_boxed_surface, normal_to_pixel_coords, normal_to_pixel_size
|
|
)
|
|
from lib.pgfw.pgfw.gfx_extension import aa_filled_polygon
|
|
|
|
class NS(Game, Animation):
|
|
"""
|
|
The main game object. It initializes and updates the title screen, boss manager, platform, dialog manager, screen
|
|
wipe manager, main character, and more (see the objects initialized in __init__). It initializes and watches the
|
|
Arduino serial port and listens for and responds to keyboard input.
|
|
"""
|
|
|
|
# Class variables that can be used to represent each of the four game pads. The L stands for "light", and the directions
|
|
# indicate which pad is being identified.
|
|
LNW, LNE, LSE, LSW = range(4)
|
|
|
|
# Class variables that can be used to represent each of the six possible orientations of the board on the four pads: the
|
|
# four sides of the square and the two diagonals.
|
|
N, NE, E, NW, S, W = range(6)
|
|
|
|
FRONT_WIDTH = 156
|
|
BACK_WIDTH = 271
|
|
LENGTH = 94
|
|
FRONT = 330
|
|
STEP = .4
|
|
IDLE_TIMEOUT = 60000 * 5
|
|
CHANNEL_COUNT = 8
|
|
NO_RESET_TIMEOUT = 3000
|
|
|
|
class Score:
|
|
|
|
def __init__(self, milliseconds=None, level_index=None, date=None):
|
|
self.milliseconds = milliseconds
|
|
self.level_index = level_index
|
|
if date is None:
|
|
date = datetime.datetime.now()
|
|
self.date = date
|
|
|
|
@classmethod
|
|
def from_string(cls, line: str):
|
|
fields = line.strip().split()
|
|
milliseconds, level_index = (int(field) for field in fields[:2])
|
|
date = None
|
|
if len(fields) > 2:
|
|
date = datetime.datetime.fromisoformat(fields[2])
|
|
if level_index == -1:
|
|
level_index = None
|
|
return cls(milliseconds, level_index, date)
|
|
|
|
@classmethod
|
|
def level(cls, milliseconds: int, level_index: int):
|
|
return cls(milliseconds, level_index)
|
|
|
|
@classmethod
|
|
def full(cls, milliseconds: int):
|
|
return cls(milliseconds)
|
|
|
|
@classmethod
|
|
def blank_level(cls, level_index: int):
|
|
return cls(level_index=level_index)
|
|
|
|
@classmethod
|
|
def blank_full(cls):
|
|
return cls()
|
|
|
|
def is_full(self):
|
|
return self.level_index is None
|
|
|
|
def formatted(self):
|
|
if self.milliseconds is None:
|
|
return "-:--.-"
|
|
else:
|
|
minutes, remainder = divmod(int(self.milliseconds), 60000)
|
|
seconds, fraction = divmod(remainder, 1000)
|
|
return f"{int(minutes)}:{int(seconds):02}.{fraction // 100}"
|
|
|
|
def blank(self):
|
|
return self.milliseconds is None
|
|
|
|
def serialize(self):
|
|
if self.level_index is None:
|
|
serialized_level_index = -1
|
|
else:
|
|
serialized_level_index = self.level_index
|
|
return f"{self.milliseconds} {serialized_level_index} {datetime.datetime.isoformat(self.date, 'T')}"
|
|
|
|
def __str__(self):
|
|
return self.formatted()
|
|
|
|
def __repr__(self):
|
|
return f"<Score: {self.formatted()}, level: {self.level_index}>"
|
|
|
|
def __lt__(self, other):
|
|
if self.level_index == other.level_index:
|
|
if self.milliseconds == other.milliseconds:
|
|
return False
|
|
elif self.blank() or other.blank():
|
|
return other.blank()
|
|
else:
|
|
return self.milliseconds < other.milliseconds
|
|
else:
|
|
if self.is_full() or other.is_full():
|
|
return other.is_full()
|
|
else:
|
|
return self.level_index < other.level_index
|
|
|
|
class Peer:
|
|
"""
|
|
Scrapeboard game on the local area network. It is expected to be sending and receiving messages using socket
|
|
communication. It will be read and written to regularly in a separate thread.
|
|
"""
|
|
status = None
|
|
result = None
|
|
versus = False
|
|
level = None
|
|
seed = None
|
|
|
|
def __init__(self, address, port):
|
|
self.address = address
|
|
self.port = port
|
|
|
|
def __init__(self):
|
|
"""
|
|
Parse the command line, set config types, initialize the serial reader, subscribe to events, and initialize child objects.
|
|
"""
|
|
# Specify possible arguments and parse the command line. If the -h flag is passed, the argparse library will print a
|
|
# help message and end the program.
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--minimize-load-time", action="store_true", help="Disable some graphics loading and effects generation")
|
|
parser.add_argument("--serial-port")
|
|
parser.add_argument("--audio-buffer-size", type=int, default=1024)
|
|
parser.add_argument("--list-serial-ports", action="store_true")
|
|
parser.add_argument("--no-serial", action="store_true", help="Force serial (Arduino) mode off.")
|
|
parser.add_argument(
|
|
"--pi", action="store_true",
|
|
help="Force to read input from the GPIO pins of a Raspberry Pi. Must be running on Raspberry Pi. Forces --no-serial.")
|
|
parser.add_argument("--show-config", action="store_true")
|
|
arguments = parser.parse_known_args()[0]
|
|
|
|
# Pre-initialize the mixer to use the specified buffer size in bytes. The default is set to 1024 to prevent lagging
|
|
# on the Raspberry Pi.
|
|
pygame.mixer.pre_init(44100, -16, 2, 1024)
|
|
|
|
# Pygame will be loaded in here.
|
|
Game.__init__(self)
|
|
|
|
# Add type declarations for non-string config name/value pairs that aren't in the default PGFW config dict.
|
|
self.get_configuration().type_declarations.add_chart({
|
|
"time":
|
|
{
|
|
"int": [
|
|
"timer-max-time", "timer-start-level-1", "timer-start-level-2", "timer-start-level-3",
|
|
"timer-start-level-4", "timer-addition-level-1", "timer-addition-level-2", "timer-addition-level-3",
|
|
"timer-addition-level-4", "sword-delay", "attract-gif-length", "attract-board-length",
|
|
"attract-reset-countdown", "level-select-reset-countdown", "level-select-press-length",
|
|
"ending-timeout", "lizard-hurt-length", "adjust-timer-start-step", "adjust-timer-addition-step",
|
|
"adjust-cooldown-step"
|
|
],
|
|
"float": [
|
|
"timer-warning-start-1", "timer-warning-start-2", "timer-warning-start-3", "timer-warning-start-4"]
|
|
},
|
|
"boss":
|
|
{
|
|
"float": [
|
|
"damage-per-hit-level-1", "damage-per-hit-level-2", "damage-per-hit-level-3",
|
|
"damage-per-hit-level-4"
|
|
],
|
|
"int": [
|
|
"cooldown-level-1", "cooldown-level-2", "cooldown-level-3", "cooldown-level-4",
|
|
"first-combo-delay"
|
|
]
|
|
},
|
|
"network":
|
|
{
|
|
"int": ["port", "diagnostics-size", "join-time-limit"],
|
|
"bool": "diagnostics",
|
|
"path": "diagnostics-font",
|
|
"list": "peers"
|
|
},
|
|
"pop-up":
|
|
{
|
|
"int": ["size", "length"],
|
|
"bool": "center"
|
|
},
|
|
"input":
|
|
{
|
|
"bool": ["serial", "pi"]
|
|
},
|
|
"display":
|
|
{
|
|
"float": "attract-gif-alpha",
|
|
"bool": ["effects", "alpha-effect-title", "qr-static"],
|
|
"path": "scores-font",
|
|
"int": "scores-alpha"
|
|
},
|
|
"system":
|
|
{
|
|
"bool": ["minimize-load-time", "enable-level-select", "optimize-title-screen"],
|
|
"int": ["lives-boss-rush-mode", "lives-level-select-mode", "max-seed"],
|
|
"path": ["dnf-file", "scores-file"]
|
|
},
|
|
"pads":
|
|
{
|
|
"int": "center_y"
|
|
},
|
|
"select":
|
|
{
|
|
"list": "titles",
|
|
"int": "font_size",
|
|
"float": ["platform_scale", "preview_margin", "zoom_step"],
|
|
"float-list": ["platform_0_xy", "platform_1_xy", "platform_2_xy", "platform_3_xy", "preview_size"]
|
|
}
|
|
})
|
|
|
|
# If a serial port was passed on the command line, override the config file setting
|
|
if arguments.serial_port is not None:
|
|
self.get_configuration().set("input", "arduino-port", arguments.serial_port)
|
|
|
|
# Command line flag requesting minimal load time overrides config file setting
|
|
if arguments.minimize_load_time:
|
|
self.get_configuration().set("system", "minimize-load-time", True)
|
|
|
|
# Turn off effects if minimal load time is requested. Minimal load time setting overrides display effects setting.
|
|
if self.get_configuration("system", "minimize-load-time"):
|
|
self.get_configuration().set("display", "effects", False)
|
|
|
|
# Apply the no serial flag from the command line if requested
|
|
if arguments.no_serial:
|
|
self.get_configuration().set("input", "serial", False)
|
|
|
|
# Apply the pi flag from the command line if requested. This takes precedence over Arduino, so even if serial is enabled,
|
|
# force to no serial mode.
|
|
if arguments.pi:
|
|
self.get_configuration().set("input", "pi", True)
|
|
if self.get_configuration("input", "serial"):
|
|
print("Pi mode was requested, so forcing serial (Arduino) mode to off")
|
|
self.get_configuration().set("input", "serial", False)
|
|
|
|
# Print the configuration if requested on the command line
|
|
if arguments.show_config:
|
|
print(self.get_configuration())
|
|
|
|
# init Pi
|
|
if self.pi_enabled():
|
|
|
|
# Initialize GPIO interface
|
|
gpio.initialize_gpio()
|
|
|
|
# Launch a separate thread for reading the GPIO (and allowing its custom delays/sleeps). Use the daemon flag to force
|
|
# exit automatically when the main thread is killed.
|
|
self.gpio_thread = Thread(target=self.read_gpio, daemon=True)
|
|
self.gpio_thread.start()
|
|
self.gpio_data = gpio.activity()
|
|
|
|
# init Arduino
|
|
elif self.serial_enabled():
|
|
|
|
# Initialize the serial reader and launch a thread for reading from the serial port
|
|
from serial import Serial, SerialException
|
|
from serial.tools import list_ports
|
|
|
|
# If a list of serial ports was requested, print detected ports and exit.
|
|
if arguments.list_serial_ports:
|
|
if list_ports.comports():
|
|
for port in list_ports.comports():
|
|
print(f"Detected serial port: {port.device}")
|
|
else:
|
|
print("No serial ports detected")
|
|
exit()
|
|
|
|
# Open the port specified by the configuration or command line if it is found. If the specified port is not
|
|
# found, iterate through the com ports, and try to open each. If no serial port can be opened, raise an
|
|
# exception.
|
|
requested_port = self.get_configuration("input", "arduino-port")
|
|
devices = [port.device for port in list_ports.comports()]
|
|
if requested_port in devices:
|
|
self.serial_reader = Serial(requested_port, timeout=.3)
|
|
else:
|
|
if requested_port:
|
|
print(f"Could not connect with requested port {requested_port}. Searching for other ports.")
|
|
found = False
|
|
for device in devices:
|
|
try:
|
|
self.serial_reader = Serial(device, timeout=.3)
|
|
found = True
|
|
except SerialException:
|
|
print(f"Tried and failed to open connection with serial device {device}")
|
|
if not found:
|
|
raise SerialException("No usable serial port devices found. Use --no-serial for keyboard-only mode.")
|
|
print(f"Using serial device at port {self.serial_reader.port}")
|
|
self.serial_data = 0
|
|
self.reset_arduino()
|
|
|
|
# Launch a separate thread for reading serial data
|
|
self.serial_thread = Thread(target=self.read_serial, daemon=True)
|
|
self.serial_thread.start()
|
|
|
|
Animation.__init__(self, self)
|
|
|
|
# All events will pass through self.respond
|
|
self.subscribe(self.respond, KEYDOWN)
|
|
self.subscribe(self.respond, KEYUP)
|
|
self.subscribe(self.respond)
|
|
ds = self.get_display_surface()
|
|
|
|
# Child objects for managing more specific parts of the game
|
|
platform_cx = self.get_display_surface().get_width() // 2
|
|
self.platform = Platform(self, (platform_cx, self.get_configuration("pads", "center_y")))
|
|
self.tony = Tony(self)
|
|
self.logo = Logo(self)
|
|
self.title = Title(self)
|
|
self.wipe = Wipe(self)
|
|
self.dialogue = Dialogue(self)
|
|
self.chemtrails = Chemtrails(self)
|
|
self.boss = Boss(self)
|
|
self.level_select = LevelSelect(self)
|
|
self.ending = Ending(self)
|
|
|
|
# Start the score list with all blank scores
|
|
self.scores = []
|
|
blank_count = 25
|
|
for level_index in range(3):
|
|
for _ in range(blank_count):
|
|
self.scores.append(NS.Score.blank_level(level_index))
|
|
for _ in range(blank_count):
|
|
self.scores.append(NS.Score.blank_full())
|
|
self.most_recent_score = None
|
|
|
|
# Add existing scores to the list from file
|
|
path = self.get_configuration("system", "scores-file")
|
|
if os.path.exists(path):
|
|
with open(path, "rt") as score_file:
|
|
for line in score_file:
|
|
if line.strip():
|
|
self.scores.append(NS.Score.from_string(line))
|
|
|
|
# Draw the score sprites
|
|
self.title.draw_scores()
|
|
|
|
# Initialize key input buffering
|
|
self.last_press = get_ticks()
|
|
|
|
# Initialize pop-up
|
|
self.register(self.close_pop_up)
|
|
self.pop_up_font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), self.get_configuration("pop-up", "size"))
|
|
self.pop_up_text = ""
|
|
|
|
# Initialize networking. Include self as a peer located at "localhost".
|
|
self.server = socket.create_server(("", self.get_configuration("network", "port")))
|
|
self.peers = {"localhost": NS.Peer("localhost", self.get_configuration("network", "port"))}
|
|
self.peers["localhost"].versus = True
|
|
print(f"Added peer 'localhost'")
|
|
if self.get_configuration("network", "peers"):
|
|
for peer in self.get_configuration("network", "peers"):
|
|
# Store peers in a dictionary where the key is the peer address
|
|
self.peers[peer] = NS.Peer(peer, self.get_configuration("network", "port"))
|
|
print(f"Added peer '{peer}'")
|
|
# Launch separate threads for listing and posting to peers
|
|
self.listen_thread = Thread(target=self.listen_to_peers, daemon=True)
|
|
self.listen_thread.start()
|
|
self.post_thread = Thread(target=self.post_to_peers, daemon=True)
|
|
self.post_thread.start()
|
|
|
|
self.reset()
|
|
|
|
# Clear events queue
|
|
clear()
|
|
|
|
def pi_enabled(self):
|
|
return self.get_configuration("input", "pi")
|
|
|
|
def serial_enabled(self):
|
|
return self.get_configuration("input", "serial")
|
|
|
|
def read_gpio(self):
|
|
"""
|
|
Test all connections of GPIO input pins.
|
|
"""
|
|
while True:
|
|
self.gpio_data = gpio.activity()
|
|
|
|
def read_serial(self):
|
|
while True:
|
|
name = self.get_configuration("input", "arduino-port")
|
|
try:
|
|
transmission = self.serial_reader.readline().strip()
|
|
print(transmission)
|
|
except:
|
|
print("Serial not ready... passing...")
|
|
transmission = ""
|
|
if len(transmission) == 4:
|
|
try:
|
|
self.serial_data = int(transmission, 2)
|
|
except ValueError:
|
|
print("Value error checking four digit serial transmission")
|
|
self.handle_garbage(transmission)
|
|
self.reset_arduino()
|
|
self.idle_elapsed = 0
|
|
elif len(transmission) > 0:
|
|
try:
|
|
int(transmission, 2)
|
|
except ValueError:
|
|
print("Received a non-four digit serial transmission")
|
|
self.handle_garbage(transmission)
|
|
else:
|
|
self.serial_data = 0
|
|
|
|
def handle_garbage(self, transmission):
|
|
self.serial_data = 0
|
|
print("Garbage detected: %s" % transmission)
|
|
self.serial_reader.reset_input_buffer()
|
|
|
|
def reset_arduino(self):
|
|
if self.serial_enabled():
|
|
self.serial_reader.dtr = False
|
|
self.serial_reader.reset_input_buffer()
|
|
self.serial_reader.dtr = True
|
|
|
|
def apply_serial(self):
|
|
for ii, light in enumerate(self.platform.lights):
|
|
light.pressed = bool(self.serial_data & (2 ** ii))
|
|
# reset idle timer if a light is detected as pressed in serial data
|
|
if light.pressed:
|
|
self.idle_elapsed = 0
|
|
|
|
def apply_gpio(self):
|
|
"""
|
|
Check the connection status of the GPIO pins and turn on the appropriate light objects (the pads).
|
|
"""
|
|
for light_id in self.gpio_data:
|
|
# The pressed state is set to the activity state
|
|
self.platform.lights[light_id].pressed = self.gpio_data[light_id]
|
|
|
|
# Reset idle timer if a light is detected as pressed
|
|
if self.gpio_data[light_id]:
|
|
self.idle_elapsed = 0
|
|
|
|
def post_to_peers(self):
|
|
"""
|
|
Update peers with current status every 1/2 second.
|
|
"""
|
|
while True:
|
|
# Determine this game's status
|
|
if self.title.active:
|
|
status = "title"
|
|
message = status
|
|
elif self.level_select.active and self.level_select.level_index_selected is None:
|
|
status = "level select"
|
|
message = status
|
|
elif self.level_select.active and not self.level_select.level_launched:
|
|
status = "voted"
|
|
level = self.level_select.level_index_selected
|
|
self.peers["localhost"].level = level
|
|
message = f"{status} {level} {self.peers['localhost'].seed}"
|
|
elif self.level_select.active or not self.boss.battle_finished:
|
|
status = "playing"
|
|
level = self.level_select.level_index_selected
|
|
self.peers["localhost"].level = level
|
|
message = f"{status} {level}"
|
|
elif self.boss.player_defeated:
|
|
status = "lost"
|
|
self.peers["localhost"].result = None
|
|
message = status
|
|
else:
|
|
status = "complete"
|
|
result = self.most_recent_score.milliseconds
|
|
self.peers["localhost"].result = result
|
|
message = f"{status} {result}"
|
|
self.peers["localhost"].status = status
|
|
|
|
# Connect and send status message to each peer. If sending fails, pass and wait until the next iteration.
|
|
for peer in self.peers.values():
|
|
if peer.address != "localhost":
|
|
try:
|
|
socket.create_connection((peer.address, peer.port)).send(str.encode(message))
|
|
except:
|
|
pass
|
|
|
|
# Send status every 1/2 second
|
|
time.sleep(0.5)
|
|
|
|
def listen_to_peers(self):
|
|
"""
|
|
Update peer statuses by processing incoming messages on the socket server.
|
|
"""
|
|
while True:
|
|
# Use the server to receive messages. Update peer statuses as the messages come in.
|
|
read_list, write_list, except_list = select.select([self.server], [], [], 0.5)
|
|
# When there is no read list, there are no messages to accept.
|
|
if (len(read_list) > 0):
|
|
incoming = self.server.accept()
|
|
peer = self.peers[incoming[1][0]]
|
|
# All messages are less than 64 characters
|
|
message = incoming[0].recv(64).decode()
|
|
if message.startswith("title") or message.startswith("level select"):
|
|
peer.versus = False
|
|
if message.startswith("complete"):
|
|
try:
|
|
peer.result = int(message.split()[-1])
|
|
peer.status = "complete"
|
|
except:
|
|
# Improperly formatted message received
|
|
pass
|
|
elif message.startswith("playing"):
|
|
try:
|
|
peer.level = int(message.split()[-1])
|
|
peer.status = "playing"
|
|
except:
|
|
pass
|
|
elif message.startswith("voted"):
|
|
try:
|
|
status, level, seed = message.split()
|
|
peer.level = int(level)
|
|
peer.seed = int(seed)
|
|
peer.status = status
|
|
except:
|
|
pass
|
|
else:
|
|
peer.status = message
|
|
|
|
def count_players(self):
|
|
"""
|
|
@return count of peers committed to a match with this peer
|
|
"""
|
|
count = 0
|
|
for peer in self.peers.values():
|
|
count += peer.versus
|
|
return count
|
|
|
|
def count_lobby(self):
|
|
"""
|
|
@return count of peers at the level select screen
|
|
"""
|
|
count = 0
|
|
for peer in self.peers.values():
|
|
count += peer.status == "level select" or peer.status == "voted"
|
|
return count
|
|
|
|
def reset(self, leave_wipe_running=False):
|
|
self.idle_elapsed = 0
|
|
self.suppressing_input = False
|
|
self.level_select.reset()
|
|
self.title.reset()
|
|
if not leave_wipe_running:
|
|
self.wipe.reset()
|
|
self.boss.reset()
|
|
self.chemtrails.reset()
|
|
self.platform.reset()
|
|
self.dialogue.reset()
|
|
self.ending.reset()
|
|
self.no_reset_elapsed = 0
|
|
self.title.activate()
|
|
|
|
def suppress_input(self):
|
|
self.suppressing_input = True
|
|
# self.platform.unpress()
|
|
|
|
def unsuppress_input(self):
|
|
self.suppressing_input = False
|
|
|
|
def respond(self, event):
|
|
"""
|
|
Respond to keyboard input.
|
|
|
|
___ ___
|
|
| O| P| These keyboard keys correspond to the floor pads.
|
|
|___|___| (O = top left pad, P = top right pad, L = bottom left pad, ; = bottom right pad)
|
|
| L| ;| Arrow keys can also be used.
|
|
|___|___| (UP = top left pad, RIGHT = top right pad, DOWN = bottom left pad, LEFT = bottom right pad)
|
|
|
|
The Z key is a shortcut for reset (F8 also resets).
|
|
|
|
The A key force resets the connected Arduino (or does nothing if no Arduino is connected).
|
|
|
|
CTRL+N toggles the display of live network diagnostics.
|
|
|
|
The following keys adjust level 4's timing parameters:
|
|
|
|
CTRL+T - timer down
|
|
CTRL+Y - timer up
|
|
|
|
CTRL+U - timer addition down
|
|
CTRL+I - timer addition up
|
|
|
|
CTRL+O - cooldown down
|
|
CTRL+P - cooldown up
|
|
|
|
@param event The Pygame event passed in automatically from pgfw.Delegate
|
|
"""
|
|
# Check key presses, limit the amount of presses per second, and reset idle timer
|
|
if not self.suppressing_input and event.type in (KEYDOWN, KEYUP):
|
|
if self.last_press <= get_ticks() - int(self.get_configuration("input", "buffer")):
|
|
self.idle_elapsed = 0
|
|
|
|
# Check keys corresponding to the platform
|
|
pressed = True if event.type == KEYDOWN else False
|
|
lights = self.platform.lights
|
|
if event.key in (K_UP, K_o):
|
|
lights[NS.LNW].pressed = pressed
|
|
elif event.key in (K_RIGHT, K_p):
|
|
lights[NS.LNE].pressed = pressed
|
|
elif event.key in (K_DOWN, K_SEMICOLON):
|
|
lights[NS.LSE].pressed = pressed
|
|
elif event.key in (K_LEFT, K_l):
|
|
lights[NS.LSW].pressed = pressed
|
|
|
|
# Check shortcut keys
|
|
elif event.key == K_z:
|
|
self.reset()
|
|
elif event.key == K_a:
|
|
self.reset_arduino()
|
|
|
|
# Check admin key combos which require CTRL+SHIFT to be held
|
|
if event.type == KEYDOWN and pygame.key.get_mods() & pygame.KMOD_CTRL:
|
|
|
|
# Toggle visibility of network diagnostics menu and pop up a message with current state
|
|
if event.key == K_n:
|
|
state = self.get_configuration("network", "diagnostics")
|
|
self.configuration.set("network", "diagnostics", not state)
|
|
self.pop_up(f"Network diagnostics visible: {not state}")
|
|
|
|
# Adjust level 4 timing parameters and pop up a message with new value
|
|
elif event.key in (K_t, K_y, K_u, K_i, K_o, K_p):
|
|
level = 4
|
|
if event.key in (K_t, K_y):
|
|
option = f"timer-start-level-{level}"
|
|
step = self.get_configuration("time", "adjust-timer-start-step")
|
|
new = self.get_configuration("time", option) + (-step if event.key == K_t else step)
|
|
self.configuration.set("time", option, str(new))
|
|
elif event.key in (K_u, K_i):
|
|
option = f"timer-addition-level-{level}"
|
|
step = self.get_configuration("time", "adjust-timer-addition-step")
|
|
new = self.get_configuration("time", option) + (-step if event.key == K_u else step)
|
|
self.configuration.set("time", option, str(new))
|
|
elif event.key in (K_o, K_p):
|
|
option = f"cooldown-level-{level}"
|
|
step = self.get_configuration("time", "adjust-cooldown-step")
|
|
new = self.get_configuration("boss", option) + (-step if event.key == K_o else step)
|
|
self.configuration.set("boss", option, str(new))
|
|
self.pop_up(f"{option}: {new}", True)
|
|
|
|
# Save the time of the key press to limit the amount of presses per second
|
|
self.last_press = get_ticks()
|
|
|
|
# Check PGFW custom commands
|
|
else:
|
|
if self.get_delegate().compare(event, "reset-game"):
|
|
self.reset()
|
|
|
|
def pop_up(self, text, clear=False):
|
|
"""
|
|
Trigger a pop up message that displays for a certain amount of time before being closed automatically. Adds a line of
|
|
text to a variable that contains all pop up messages in case there is a previously sent message that needs to continue
|
|
being displayed.
|
|
|
|
@param text message to display
|
|
@param clear if True, delete any existing messages
|
|
"""
|
|
if not clear:
|
|
self.pop_up_text += f"{text}\n"
|
|
else:
|
|
self.pop_up_text = f"{text}\n"
|
|
self.halt(self.close_pop_up)
|
|
self.play(self.close_pop_up, play_once=True, delay=3000)
|
|
|
|
def close_pop_up(self):
|
|
"""
|
|
Close the pop up message by removing all text from the pop up text variable. This will cause the pop up to stop being
|
|
drawn each frame.
|
|
"""
|
|
self.pop_up_text = ""
|
|
|
|
def add_time_to_scores(self, milliseconds: int, level_index=None):
|
|
"""
|
|
Add a time to the list of scores. This method will build a score object, add it to the list, and write to the scores file.
|
|
It will also call on the title screen object to draw the sprites.
|
|
|
|
@param milliseconds player's time in milliseconds
|
|
@param level_index the level this time corresponds to or None for a full game
|
|
"""
|
|
if level_index is None:
|
|
score = NS.Score.full(milliseconds)
|
|
else:
|
|
score = NS.Score.level(milliseconds, level_index)
|
|
self.scores.append(score)
|
|
self.most_recent_score = score
|
|
|
|
# Write scores to file
|
|
try:
|
|
with open(self.get_configuration("system", "scores-file"), "wt") as score_file:
|
|
for score in sorted(self.scores):
|
|
if not score.blank():
|
|
score_file.write(f"{score.serialize()}\n")
|
|
except:
|
|
print("Error saving scores")
|
|
|
|
self.title.draw_scores()
|
|
|
|
def update(self):
|
|
Animation.update(self)
|
|
last_frame_duration = self.time_filter.get_last_frame_duration()
|
|
|
|
# Apply controller input to light (pad) states from either Pi or Arduino if applicable
|
|
if self.pi_enabled():
|
|
|
|
# Translate Raspberry Pi GPIO state into pad states
|
|
self.apply_gpio()
|
|
|
|
elif self.serial_enabled():
|
|
|
|
# Translate the most recent serial data, being provided by serial/serial.ino, into pad states
|
|
self.apply_serial()
|
|
|
|
# Handle auto reset of the Arduino for stablizing serial data
|
|
if self.title.active or self.ending.active or self.dialogue.active:
|
|
self.no_reset_elapsed += last_frame_duration
|
|
|
|
# If we received good input, reset the auto reset timer
|
|
if 0b11 <= self.serial_data <= 0b1100:
|
|
self.no_reset_elapsed = 0
|
|
if self.no_reset_elapsed >= self.NO_RESET_TIMEOUT:
|
|
print("auto arduino reset triggered")
|
|
self.reset_arduino()
|
|
self.no_reset_elapsed = 0
|
|
|
|
self.title.update()
|
|
self.level_select.update()
|
|
self.ending.update()
|
|
self.boss.update()
|
|
if not self.title.active:
|
|
self.platform.update()
|
|
self.chemtrails.update()
|
|
self.boss.update_dialogue()
|
|
self.wipe.update()
|
|
|
|
# Draw pop up text line by line
|
|
if self.pop_up_text:
|
|
width = 0
|
|
height = 0
|
|
for line in self.pop_up_text.split("\n"):
|
|
if line:
|
|
line_width, line_height = self.pop_up_font.size(line)
|
|
if line_width > width:
|
|
width = line_width
|
|
height += line_height
|
|
full_surface = pygame.Surface((width, height))
|
|
x = 0
|
|
y = 0
|
|
for line in self.pop_up_text.split("\n"):
|
|
if line:
|
|
surface = self.pop_up_font.render(
|
|
line, True, pygame.Color(self.get_configuration("pop-up", "foreground")),
|
|
pygame.Color(self.get_configuration("pop-up", "background")))
|
|
full_surface.blit(surface, (x, y))
|
|
y += surface.get_height()
|
|
if y > 0:
|
|
sprite = Sprite(self)
|
|
sprite.add_frame(full_surface)
|
|
sprite.set_alpha(200)
|
|
if self.get_configuration("pop-up", "center"):
|
|
sprite.location.center = self.get_display_surface().get_rect().center
|
|
sprite.update()
|
|
|
|
# Draw network diagnostics
|
|
if self.get_configuration("network", "diagnostics"):
|
|
y = self.get_display_surface().get_rect().bottom
|
|
font = pygame.font.Font(self.get_configuration("network", "diagnostics-font"),
|
|
self.get_configuration("network", "diagnostics-size"))
|
|
for peer in self.peers.values():
|
|
surface = font.render(
|
|
f"{peer.address} {peer.status} [PvP {peer.versus}, lvl {peer.level}, result {peer.result}]",
|
|
True, (255, 255, 255), (0, 0, 0))
|
|
surface.set_alpha(200)
|
|
y -= surface.get_height()
|
|
self.get_display_surface().blit(surface, (0, y))
|
|
surface = font.render(f"players: {self.count_players()} lobby: {self.count_lobby()}", True, (255, 255, 255), (0, 0, 0))
|
|
surface.set_alpha(200)
|
|
y -= surface.get_height()
|
|
self.get_display_surface().blit(surface, (0, y))
|
|
|
|
# Reset the game when idle
|
|
self.idle_elapsed += self.time_filter.get_last_frame_duration()
|
|
if self.idle_elapsed >= self.IDLE_TIMEOUT:
|
|
self.reset()
|
|
|
|
def end(self, event):
|
|
"""
|
|
Extend the parent end method to try adding a permanent quit feature in case there is a Raspbian Lite systemd autostart
|
|
service running
|
|
"""
|
|
if event.type == QUIT or self.delegate.compare(event, "quit"):
|
|
if self.confirming_quit or not self.get_configuration("input", "confirm-quit"):
|
|
# If SHIFT is pressed, try permanently stopping the systemd service to get a console back in case this is running on
|
|
# Raspbian Lite
|
|
if pygame.key.get_mods() & pygame.KMOD_SHIFT:
|
|
try:
|
|
subprocess.run(["sudo", "systemctl", "stop", "scrapeboard"])
|
|
print("Killing with permanent stop sent to systemd scrapeboard service")
|
|
except:
|
|
print("No scrapeboard system service detected, so permanent quit either failed or was unnecessary")
|
|
|
|
# Call parent to complete quit
|
|
super().end(event)
|
|
|
|
|
|
class LevelSelect(Animation):
|
|
"""
|
|
Display the available levels. Initialize a platform for each level and display each platform beneath its level glowing
|
|
with a pair of pads to press to start that level. Wait for user input, then launch the level of the pair that gets
|
|
pressed by the user.
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
Animation.__init__(self, parent)
|
|
self.subscribe(self.respond, KEYDOWN)
|
|
self.register(self.timeout, self.force_launch)
|
|
|
|
# Create new platforms, scaled for display, place them on screen, and set the appropriate pads glowing to
|
|
# indicate how to select the associated level.
|
|
self.init_platforms()
|
|
|
|
# Create frames for each level preview. Draw the level's text and boss image on the frame. Place the platform
|
|
# below the level preview frame.
|
|
self.init_previews()
|
|
|
|
# Deactivate
|
|
self.reset()
|
|
|
|
def init_platforms(self):
|
|
"""
|
|
Create a list of platforms, one for each level on the select screen. Scale the platforms to fit on the screen.
|
|
Place each on screen, and set the appropriate pads glowing to indicate how to select the level.
|
|
"""
|
|
# Create new platforms, and center them at the configured locations
|
|
self.platforms = []
|
|
for platform_index in range(4):
|
|
self.platforms.append(Platform(self, normal_to_pixel_coords(
|
|
Vector(*self.get_configuration("select", f"platform_{platform_index}_xy")))))
|
|
|
|
# Scale the platform for display on the level select
|
|
scale = self.get_configuration("select", "platform_scale")
|
|
for platform in self.platforms:
|
|
for ii, frame in enumerate(platform.view.frames):
|
|
scaled = pygame.transform.smoothscale(
|
|
frame, (int(frame.get_width() * scale), int(frame.get_height() * scale)))
|
|
platform.view.frames[ii] = scaled
|
|
platform.view.get_current_frameset().measure_rect()
|
|
platform.view.update_location_size()
|
|
|
|
# Indicate which pads to press to choose a level
|
|
self.platforms[0].set_glowing((NS.LNW, NS.LSE))
|
|
self.platforms[1].set_glowing((NS.LNW, NS.LSW))
|
|
self.platforms[2].set_glowing((NS.LNW, NS.LNE))
|
|
self.platforms[3].set_glowing((NS.LNE, NS.LSE))
|
|
|
|
def init_previews(self):
|
|
"""
|
|
Create outer frames for each level preview. Draw the level name and boss image on the frame. Place the
|
|
platform below the level preview frame.
|
|
"""
|
|
preview_rect = pygame.Rect((0, 0), normal_to_pixel_size(
|
|
Vector(*self.get_configuration("select", "preview_size"))))
|
|
self.previews = []
|
|
font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), self.get_configuration("select", "font_size"))
|
|
padding = 4
|
|
for platform_index in range(len(self.platforms)):
|
|
self.previews.append(Sprite(self, 100))
|
|
text = font.render(self.get_configuration("select", "titles")[platform_index], True, (0, 0, 0))
|
|
text = pygame.transform.rotate(text, 90)
|
|
text_rect = text.get_rect()
|
|
text_rect.midleft = preview_rect.midleft
|
|
frame = self.get_game().boss.backgrounds[platform_index].frames[0]
|
|
frame_rect = (preview_rect.w - text_rect.w - padding, preview_rect.h - padding * 2)
|
|
environment = pygame.transform.smoothscale(frame, frame_rect)
|
|
environment_rect = environment.get_rect()
|
|
environment_rect.midright = preview_rect.right - padding, preview_rect.centery
|
|
boss = pygame.transform.smoothscale(self.get_game().boss.level_sprite(platform_index).frames[0],
|
|
environment_rect.inflate(-64, -28).size)
|
|
boss_rect = boss.get_rect()
|
|
boss_rect.center = environment_rect.center
|
|
for hue in range(0, 360, 8):
|
|
frame = pygame.Surface(preview_rect.size)
|
|
color = Color(0, 0, 0)
|
|
color.hsla = hue, 100, 75, 100
|
|
frame.fill(color)
|
|
frame.blit(text, text_rect)
|
|
frame.blit(environment, environment_rect)
|
|
frame.blit(boss, boss_rect)
|
|
self.previews[-1].add_frame(frame)
|
|
self.previews[-1].location.midbottom = self.platforms[platform_index].view.location.centerx, \
|
|
self.platforms[platform_index].view.location.top - normal_to_pixel_size(
|
|
Vector(0, self.get_configuration("select", "preview_margin"))).y
|
|
|
|
def activate(self):
|
|
"""
|
|
Flip the `active` flag on, so relevant functions like `update` will know to run. Reset the animations for
|
|
platforms and previews, so they will play in sync. Start counting down the idle timeout clock.
|
|
"""
|
|
self.reset()
|
|
self.active = True
|
|
for platform in self.platforms:
|
|
platform.activate()
|
|
platform.view.get_current_frameset().reset()
|
|
platform.view.play()
|
|
for preview in self.previews:
|
|
preview.get_current_frameset().reset()
|
|
preview.play()
|
|
self.start_timeout_countdown()
|
|
|
|
def deactivate(self):
|
|
self.active = False
|
|
for platform in self.platforms:
|
|
platform.deactivate()
|
|
self.level_index_selected = None
|
|
self.level_launched = False
|
|
self.launch_forced = False
|
|
self.zoom = 1.0
|
|
self.grow_sound_channel = None
|
|
for index in range(len(self.platforms)):
|
|
self.platforms[index].view.halt(self.platforms[index].view.wipe_out)
|
|
self.previews[index].halt(self.previews[index].wipe_out)
|
|
self.platforms[index].view.reset()
|
|
self.previews[index].reset()
|
|
self.platforms[index].view.unhide()
|
|
self.previews[index].unhide()
|
|
self.halt()
|
|
|
|
def reset(self):
|
|
self.deactivate()
|
|
|
|
def respond(self, event):
|
|
"""
|
|
Respond to CTRL + key presses to launch a level or toggle level select mode
|
|
"""
|
|
level_index = None
|
|
if pygame.key.get_mods() & pygame.KMOD_CTRL:
|
|
if event.key in (pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4):
|
|
self.launch(event.key - pygame.K_1)
|
|
elif event.key == pygame.K_l:
|
|
level_select_enabled = not self.get_configuration("system", "enable-level-select")
|
|
self.get_configuration().set("system", "enable-level-select", level_select_enabled)
|
|
self.get_game().pop_up(f"Level select mode set to {level_select_enabled}")
|
|
|
|
def launch(self, index):
|
|
"""
|
|
Start a level through the boss object
|
|
"""
|
|
self.get_game().boss.start_level(index)
|
|
self.deactivate()
|
|
|
|
def launch_selected_index(self):
|
|
"""
|
|
Launch level index stored in the member variable
|
|
"""
|
|
self.launch(self.level_index_selected)
|
|
|
|
def start_timeout_countdown(self):
|
|
"""
|
|
Launch an animation on a delay that will reset the game after the delay. If the countdown is already active, reset the
|
|
countdown.
|
|
"""
|
|
self.halt(self.timeout)
|
|
self.play(self.timeout, delay=self.get_configuration("time", "level-select-reset-countdown"), play_once=True)
|
|
|
|
def timeout(self):
|
|
"""
|
|
Reset to the title screen
|
|
"""
|
|
self.get_game().wipe.start(self.get_game().reset, leave_wipe_running=True)
|
|
|
|
def force_launch(self):
|
|
self.launch_forced = True
|
|
|
|
def update(self):
|
|
if self.active:
|
|
Animation.update(self)
|
|
self.get_game().logo.update()
|
|
for ii, preview in enumerate(self.previews):
|
|
if ii != self.level_index_selected:
|
|
preview.update()
|
|
if self.level_index_selected is None:
|
|
for level_index, platform in enumerate(self.platforms):
|
|
if platform.get_glowing_edge() == self.get_game().platform.get_edge_pressed():
|
|
|
|
# Level has been selected
|
|
if self.get_game().platform.press_elapsed > self.get_configuration("time", "level-select-press-length"):
|
|
# This will cause a vote to be cast to peers if there are any. If there are others in the lobby,
|
|
# the game will wait for other votes to be cast or the lobby to clear. Otherwise, the level will
|
|
# launch.
|
|
self.level_index_selected = level_index
|
|
if self.grow_sound_channel is not None:
|
|
self.grow_sound_channel.stop()
|
|
self.grow_sound_channel = None
|
|
self.get_game().peers["localhost"].seed = random.randint(0, self.get_configuration("system", "max-seed"))
|
|
self.seed = self.get_game().peers['localhost'].seed
|
|
print(f"Set seed to {self.seed}")
|
|
self.play(self.force_launch, delay=self.get_configuration("network", "join-time-limit"))
|
|
# Wipe away other levels and zoom selected
|
|
for level_index in range(3):
|
|
if level_index != self.level_index_selected:
|
|
self.platforms[level_index].view.play(self.platforms[level_index].view.wipe_out)
|
|
self.previews[level_index].play(self.previews[level_index].wipe_out, interval=100)
|
|
self.get_audio().play_sfx("complete_pattern_3")
|
|
self.players_counted = 1
|
|
break
|
|
|
|
# Selection ring around level being selected is still growing
|
|
else:
|
|
if self.grow_sound_channel is None:
|
|
self.grow_sound_channel = self.get_audio().play_sfx("grow", -1, x=platform.view.location.centerx)
|
|
# Draw a growing ring around the currently pressed level
|
|
angle = self.get_game().platform.press_elapsed / \
|
|
self.get_configuration("time", "level-select-press-length") * 2 * pi
|
|
diameter = self.previews[level_index].location.height + 21
|
|
rect = pygame.Rect(0, 0, diameter, diameter)
|
|
rect.center = self.previews[level_index].location.center
|
|
offset = 0
|
|
while offset < .2:
|
|
if offset < angle:
|
|
pygame.draw.arc(self.get_display_surface(), (255, 255, 255), rect, offset, angle, 14)
|
|
offset += .01
|
|
|
|
# Check if peers are still deciding
|
|
elif not self.level_launched:
|
|
|
|
# Sync seed and wait time with players who have voted for the same level
|
|
for peer in self.get_game().peers.values():
|
|
if peer.address != "localhost" and peer.status == "voted" and peer.level == self.level_index_selected:
|
|
peer.versus = True
|
|
if self.get_game().count_players() > self.players_counted:
|
|
self.players_counted += 1
|
|
self.seed = (self.seed + peer.seed) % self.get_configuration("system", "max-seed")
|
|
self.halt(self.force_launch)
|
|
self.play(self.force_launch, delay=self.get_configuration("network", "join-time-limit"))
|
|
|
|
# Launch if time is up, the lobby is empty, or everyone present has voted
|
|
if all(peer.status != "level select" or peer.status == "voted" for peer in self.get_game().peers.values()) or \
|
|
self.launch_forced:
|
|
print(f"Seeding generator with {self.seed}")
|
|
random.seed(self.seed)
|
|
self.halt(self.force_launch)
|
|
self.get_game().pop_up("", clear=True)
|
|
self.level_launched = True
|
|
self.opponents_at_launch = [peer for peer in self.get_game().peers.values() if peer.versus]
|
|
if len(self.opponents_at_launch) > 1:
|
|
for level_index in range(3):
|
|
if level_index != self.level_index_selected:
|
|
self.platforms[level_index].view.halt()
|
|
self.previews[level_index].halt()
|
|
self.get_game().wipe.start(self.launch_selected_index)
|
|
|
|
# Update displayed wait message
|
|
else:
|
|
remaining = self.accounts[self.force_launch].delay
|
|
if remaining < self.get_configuration("network", "join-time-limit") - 500:
|
|
self.get_game().pop_up(
|
|
f"Waiting {remaining // 1000 + 1}s for players to join", clear=True)
|
|
|
|
# Second half of launch animation
|
|
elif not self.get_game().wipe.is_playing() and any(preview.is_hidden() for preview in self.previews):
|
|
|
|
# Final animation before game will launch, launch is attached to the animation and will be triggered automatically
|
|
self.get_game().wipe.start(self.launch_selected_index)
|
|
|
|
for platform in self.platforms:
|
|
platform.update()
|
|
if self.level_index_selected is not None:
|
|
preview = self.previews[self.level_index_selected]
|
|
self.zoom += self.get_configuration("select", "zoom_step")
|
|
frame = pygame.transform.scale(
|
|
preview.get_current_frame(), (int(preview.location.w * self.zoom), int(preview.location.h * self.zoom)))
|
|
rect = frame.get_rect()
|
|
rect.center = preview.location.center
|
|
preview.update()
|
|
self.get_display_surface().blit(frame, rect)
|
|
# If input in the player's platform detected reset the automatic game reset countdown
|
|
if self.get_game().platform.get_pressed():
|
|
self.start_timeout_countdown()
|
|
elif self.grow_sound_channel is not None:
|
|
self.grow_sound_channel.stop()
|
|
self.grow_sound_channel = None
|
|
|
|
|
|
class Button(Sprite):
|
|
|
|
MARGIN = 2
|
|
BLANK = (200, 200, 200)
|
|
|
|
def __init__(self, parent, edge, size, border):
|
|
Sprite.__init__(self, parent)
|
|
colors = self.get_game().platform.get_color_pair_from_edge(edge)
|
|
width = size * 2 + self.MARGIN + border * 4
|
|
step = width / 2 + self.MARGIN / 2
|
|
rect_width = width / 2 - self.MARGIN / 2
|
|
rects = Rect(0, 0, rect_width, rect_width), \
|
|
Rect(step, 0, rect_width, rect_width), \
|
|
Rect(step, step, rect_width, rect_width), \
|
|
Rect(0, step, rect_width, rect_width)
|
|
if edge == NS.N:
|
|
colored = rects[0], rects[1]
|
|
elif edge == NS.NE:
|
|
colored = rects[1], rects[3]
|
|
elif edge == NS.E:
|
|
colored = rects[1], rects[2]
|
|
elif edge == NS.NW:
|
|
colored = rects[0], rects[2]
|
|
elif edge == NS.S:
|
|
colored = rects[3], rects[2]
|
|
elif edge == NS.W:
|
|
colored = rects[0], rects[3]
|
|
for lightness in range(30, 90, 5):
|
|
frame = Surface((width, width), SRCALPHA)
|
|
for topleft in (0, 0), (step, 0), (step, step), (0, step):
|
|
rect = Rect(topleft, (rect_width, rect_width))
|
|
border_color = Color(*self.BLANK)
|
|
border_color.a = 179
|
|
frame.fill(border_color, rect)
|
|
frame.fill((0, 0, 0, 0), rect.inflate(-border * 2, -border * 2))
|
|
for ii in range(2):
|
|
original_color = Color(*colors[ii])
|
|
original_color.a = 255
|
|
edited_color = Color(0, 0, 0)
|
|
edited_color.hsla = int(original_color.hsla[0]), int(original_color.hsla[1]), \
|
|
lightness, 70
|
|
frame.fill(edited_color, colored[ii])
|
|
frame.fill(original_color, colored[ii].inflate(-border * 2, -border * 2))
|
|
self.add_frame(frame)
|
|
|
|
|
|
class Meter(GameChild):
|
|
|
|
SPACING = 12
|
|
|
|
def __init__(self, parent):
|
|
GameChild.__init__(self, parent)
|
|
|
|
def setup(self, background, rect, indent, color, units, path):
|
|
self.background = background
|
|
self.rect = rect
|
|
self.icons = []
|
|
x = rect.left + indent
|
|
base = get_color_swapped_surface(
|
|
load(self.get_resource(path)).convert_alpha(),
|
|
(0, 0, 0), color)
|
|
while x <= self.rect.right - base.get_width() - self.SPACING:
|
|
icon = Sprite(self)
|
|
icon.add_frame(base)
|
|
icon.location.midleft = x, self.rect.centery
|
|
self.icons.append(icon)
|
|
x += icon.location.w + self.SPACING
|
|
self.units = units
|
|
|
|
def reset(self):
|
|
self.amount = self.units
|
|
for icon in self.icons:
|
|
icon.unhide()
|
|
|
|
def change(self, delta):
|
|
self.amount += delta
|
|
cutoff = float(self.amount) / self.units * len(self.icons)
|
|
for ii, icon in enumerate(self.icons):
|
|
if ii < cutoff:
|
|
icon.unhide()
|
|
else:
|
|
icon.hide()
|
|
|
|
def percent(self):
|
|
"""
|
|
Return amount as a percent of the full amount
|
|
"""
|
|
return self.amount / self.units
|
|
|
|
def update(self):
|
|
ds = self.get_display_surface()
|
|
ds.blit(self.background, self.rect)
|
|
for icon in self.icons:
|
|
icon.update()
|
|
|
|
|
|
class Tony(Sprite):
|
|
"""
|
|
A fullscreen-sized sprite of Tony Hawk the Birdman with animation and glow effects.
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
"""
|
|
Load board animation, create a glow effect, and load taunt sound effects.
|
|
"""
|
|
Sprite.__init__(self, parent, 100, False)
|
|
dsr = self.get_display_surface().get_rect()
|
|
self.board = Sprite(self, 100)
|
|
self.board.load_from_path(self.get_resource("newTony/TonyArms"), True)
|
|
|
|
# Create a glowing effect object by adding glow frames to a blank Sprite. It can then be applied to the main Tony Sprite frame
|
|
# using `pygame.BLEND_RGBA_SUB`. Skip this if fast load is requested.
|
|
if not self.get_configuration("system", "minimize-load-time"):
|
|
if self.get_configuration("display", "alpha-effect-title"):
|
|
self.effect = Sprite(self)
|
|
else:
|
|
self.effect = Sprite(self, 120)
|
|
for offset in range(12):
|
|
if self.get_configuration("display", "alpha-effect-title"):
|
|
w, h = dsr.w - 40, int(dsr.h * .65)
|
|
glow = Surface((w, h), SRCALPHA)
|
|
else:
|
|
w, h = dsr.w - 120, int(dsr.h * .65)
|
|
glow = Surface((w, h))
|
|
for ii, y in enumerate(range(h, 0, -8)):
|
|
hue = range(240, 200, -2)[(ii - offset) % 12]
|
|
alpha = min(100, int(round(y / float(h - 10) * 100)))
|
|
color = get_hsla_color(hue, 100, 50, alpha)
|
|
if ii == 0:
|
|
aaellipse(glow, w // 2, y, w // 2 - 4, h // 20, color)
|
|
ellipse(glow, w // 2, y, w // 2 - 4, h // 20, color)
|
|
filled_ellipse(glow, w // 2, y, w // 2 - 4, h // 20, color)
|
|
self.effect.add_frame(glow)
|
|
if self.get_configuration("display", "alpha-effect-title"):
|
|
self.effect.location.topleft = -20, int(dsr.h * .35)
|
|
else:
|
|
self.effect.location.midbottom = dsr.midbottom
|
|
|
|
self.add_frame(load(self.get_resource("Big_Tony.png")).convert_alpha())
|
|
self.load_from_path(self.get_resource("newTony/TonyShirtHead"), True)
|
|
self.add_frameset([0], name="static")
|
|
self.add_frameset(range(1, len(self.frames)), name="board")
|
|
self.taunts = []
|
|
for sfx_name in self.get_audio().sfx:
|
|
if sfx_name.startswith("TonyTauntsBend_"):
|
|
self.taunts.append(sfx_name)
|
|
self.location.centerx = dsr.centerx
|
|
self.board.location.centerx = self.location.centerx
|
|
|
|
# Add a QR code to the t-shirt
|
|
self.qr = Sprite(self)
|
|
frame = pygame.image.load(self.get_resource("qr/qr.png"))
|
|
frame = pygame.transform.smoothscale(frame, (165, 141))
|
|
self.qr.add_frame(frame)
|
|
self.qr.location.midtop = 410, 103
|
|
self.qr_text = Sprite(self, [6000, 1500, 1500, 1500, 1500])
|
|
if self.get_configuration("display", "qr-static"):
|
|
frames = load_frames(self.get_resource("qr/qr_text_static.png"), transparency=True)
|
|
else:
|
|
frames = load_frames(self.get_resource("qr/"), query="qr_text_[0-9].png", transparency=True)
|
|
for ii, frame in enumerate(frames):
|
|
frames[ii] = pygame.transform.smoothscale(frame, (165, int(165 / frame.get_width() * frame.get_height())))
|
|
self.qr_text.add_frames(frames)
|
|
self.qr_text.location.midtop = self.qr.location.midbottom
|
|
|
|
def set_frameset(self, name):
|
|
Sprite.set_frameset(self, name)
|
|
self.get_current_frameset().reset()
|
|
self.set_framerate(100)
|
|
if name == "board":
|
|
self.board.get_current_frameset().reset()
|
|
self.board.unhide()
|
|
self.board.set_framerate(100)
|
|
self.board.halt()
|
|
elif name == "static":
|
|
self.board.hide()
|
|
|
|
def shift_frame(self):
|
|
Sprite.shift_frame(self)
|
|
frameset = self.get_current_frameset()
|
|
if frameset.name == "board" and frameset.current_index == 1:
|
|
self.get_audio().play_sfx(random.choice(self.taunts))
|
|
|
|
def update(self):
|
|
"""
|
|
Apply the glow effect using an intermediate surface to blend the glow effect with the current main sprite frame. Skip the effect if
|
|
effects are off. Update title screen objects. Update the board sub-animation if it is active.
|
|
"""
|
|
# Create an intermediate surface for blending the glow with the sprite frame
|
|
if self.get_configuration("display", "alpha-effect-title"):
|
|
save = self.get_display_surface()
|
|
intermediate_surface = Surface(self.location.size, SRCALPHA)
|
|
self.display_surface = intermediate_surface
|
|
self.qr.display_surface = intermediate_surface
|
|
self.qr_text.display_surface = intermediate_surface
|
|
location_save = self.location.copy()
|
|
self.location.topleft = 0, 0
|
|
self.qr.location.centerx = self.location.centerx + 10
|
|
self.qr_text.location.midtop = self.qr.location.midbottom
|
|
|
|
# Do a regular Sprite animation update
|
|
Sprite.update(self)
|
|
self.qr.update()
|
|
self.qr_text.update()
|
|
|
|
# Blend the effect frame with the sprite frame
|
|
if not self.get_configuration("system", "minimize-load-time"):
|
|
if self.get_configuration("display", "alpha-effect-title"):
|
|
self.display_surface = save
|
|
self.location = location_save
|
|
self.effect.display_surface = intermediate_surface
|
|
self.effect.update(flags=BLEND_RGBA_SUB)
|
|
self.get_display_surface().blit(intermediate_surface, self.location.topleft)
|
|
else:
|
|
self.effect.update(flags=BLEND_RGBA_SUB)
|
|
|
|
# Update title screen objects that are drawn over this sprite
|
|
if self.get_game().title.active:
|
|
self.get_game().title.video.update()
|
|
self.get_game().platform.update()
|
|
self.get_game().chemtrails.update()
|
|
|
|
# Update the board sub-animation
|
|
frameset = self.get_current_frameset()
|
|
if frameset.name == "board":
|
|
self.board.get_current_frameset().current_index = frameset.current_index
|
|
if frameset.current_index == len(frameset.order) - 1:
|
|
self.set_framerate(3000)
|
|
else:
|
|
self.set_framerate(100)
|
|
self.board.update()
|
|
|
|
|
|
class Video(Sprite):
|
|
"""
|
|
Attract mode pop-up that rotates through GIFs.
|
|
"""
|
|
|
|
def __init__(self, parent, diameter, next_video_chance=.01):
|
|
Sprite.__init__(self, parent, 100)
|
|
self.next_video_chance = next_video_chance
|
|
pattern = join(self.get_resource("gif"), "Boarding_*.gif")
|
|
gifs = []
|
|
for path in iglob(pattern):
|
|
gifs.append(Image.open(path))
|
|
print(gifs[-1].info)
|
|
self.gif_index = 0
|
|
mask = Surface([diameter] * 2, SRCALPHA)
|
|
rect = mask.get_rect()
|
|
alpha = int(self.get_configuration("display", "attract-gif-alpha") * 255)
|
|
filled_circle(mask, rect.centerx, rect.centery, rect.centerx, (0, 0, 0, alpha))
|
|
filled_circle(mask, rect.centerx, rect.centery, rect.centerx - 2, (255, 255, 255, alpha))
|
|
self.add_frame(mask)
|
|
if not self.get_configuration("system", "minimize-load-time"):
|
|
self.play()
|
|
# preload GIF frames scaled instead of loading each frame like before
|
|
self.gif_frames_scaled = []
|
|
for gif in gifs:
|
|
self.gif_frames_scaled.append([])
|
|
for ii in range(0, gif.n_frames):
|
|
gif.seek(ii)
|
|
frame_scaled = smoothscale(
|
|
fromstring(gif.convert("RGBA").tobytes(), gif.size, "RGBA"),
|
|
(mask.get_width(), int(gif.width * gif.height / mask.get_width())))
|
|
copy = mask.copy()
|
|
rect = frame_scaled.get_rect()
|
|
rect.bottom = copy.get_rect().bottom
|
|
copy.blit(frame_scaled, rect, None, BLEND_RGBA_MIN)
|
|
self.gif_frames_scaled[-1].append(copy)
|
|
self.load_selection()
|
|
|
|
def load_selection(self):
|
|
self.clear_frames()
|
|
for frame in self.gif_frames_scaled[self.gif_index]:
|
|
self.add_frame(frame)
|
|
|
|
def shift_frame(self):
|
|
Sprite.shift_frame(self)
|
|
if random.random() < self.next_video_chance:
|
|
while True:
|
|
selection = random.choice(range(0, len(self.gif_frames_scaled)))
|
|
if selection != self.gif_index:
|
|
self.gif_index = selection
|
|
self.load_selection()
|
|
break
|
|
|
|
|
|
class Logo(Sprite):
|
|
"""
|
|
A screen-sized layer displaying the logo tile-filled. Hacked into displaying only a single frame for performance.
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
"""
|
|
Load the logo and create a glowing version by creating multiple frames, each with a glow effect blended onto it. But disable the
|
|
glow and create a static background frame as a hack.
|
|
"""
|
|
Sprite.__init__(self, parent, 60)
|
|
dsr = self.get_display_surface().get_rect()
|
|
mask = pygame.image.load(self.get_resource("Title_tile.png")).convert()
|
|
palette = (255, 255, 255), (255, 255, 128), (255, 255, 0)
|
|
thickness = 8
|
|
for offset in range(len(palette)):
|
|
tile = mask.copy()
|
|
for x in range(0, dsr.w, thickness):
|
|
tile.fill(palette[(offset + x) % len(palette)], (x, 0, thickness, dsr.h), pygame.BLEND_RGB_MIN)
|
|
self.add_frame(tile)
|
|
for y in range(0, dsr.h + self.location.h, self.location.h):
|
|
for x in range(0, dsr.w + self.location.w, self.location.w):
|
|
if x != 0 or y != 0:
|
|
self.add_location((x, y))
|
|
|
|
def update(self):
|
|
"""
|
|
"""
|
|
self.move(-1, 1)
|
|
if self.location.right < 0:
|
|
self.move(self.location.w)
|
|
if self.location.top > 0:
|
|
self.move(dy=-self.location.h)
|
|
Sprite.update(self)
|
|
|
|
|
|
class Title(Animation):
|
|
"""
|
|
Handles displaying and drawing the title screen. It draws the high scores, creates and updates an attract mode video pop-up, tracks
|
|
the player's moves and checks if they are doing the start game pattern, and updates the background logo and giant Tony sprite.
|
|
|
|
Title.draw_scores is a slow method, so the scores should only be drawn when a score is added.
|
|
|
|
If the game is configured to optimize on the title screen, the scores will only be blit when Title.activate is called. Otherwise,
|
|
they will blit every update.
|
|
"""
|
|
|
|
# Sequence of moves the player must do to start the game
|
|
UNLOCK_MOVES = NS.NW, NS.N, NS.NE, NS.S
|
|
|
|
def __init__(self, parent):
|
|
"""
|
|
Create an object for the attract mode video pop-up.
|
|
|
|
@param parent GameChild object that will connect this GameChild object to the overall tree and root Game object
|
|
"""
|
|
Animation.__init__(self, parent)
|
|
self.active = False
|
|
|
|
# Set up attract mode pop-up
|
|
self.angle = pi / 8
|
|
self.video = Video(self, 320)
|
|
self.video.location.center = 329, 182
|
|
self.register(self.show_video, self.hide_video)
|
|
self.show_video()
|
|
|
|
# Set up scores
|
|
font_path = self.get_resource(self.get_configuration("display", "scores-font"))
|
|
self.heading_font = pygame.font.Font(font_path, 22)
|
|
self.score_font = pygame.font.Font(font_path, 16)
|
|
self.score_sprites = []
|
|
|
|
def reset(self):
|
|
"""
|
|
Set the unlock progress back to zero. Start glowing the first move in the unlock pattern. Halt all animations. Unhide video.
|
|
"""
|
|
self.unlock_index = 0
|
|
self.get_game().platform.set_glowing(self.get_game().platform.get_buttons_from_edges([self.UNLOCK_MOVES[0]]))
|
|
self.halt()
|
|
self.show_video()
|
|
|
|
def activate(self):
|
|
"""
|
|
Activate platform, Tony, and player objects as well. Start playing the BGM.
|
|
"""
|
|
self.active = True
|
|
platform = self.get_game().platform
|
|
platform.activate()
|
|
platform.set_glowing(platform.get_buttons_from_edges([self.UNLOCK_MOVES[self.unlock_index]]))
|
|
self.get_game().chemtrails.activate()
|
|
self.get_game().tony.set_frameset("static")
|
|
self.get_audio().play_bgm("title")
|
|
|
|
# Optimization for only drawing part of the title screen
|
|
if self.get_configuration("system", "optimize-title-screen"):
|
|
|
|
# Blit the scores
|
|
for sprite in self.score_sprites:
|
|
sprite.update()
|
|
|
|
# Optimize by setting a clip that excludes the area where the scores are drawn
|
|
self.get_display_surface().set_clip(
|
|
(self.score_sprites[0].location.right, 0, self.score_sprites[1].location.left - self.score_sprites[0].location.right,
|
|
self.get_display_surface().get_height()))
|
|
|
|
def deactivate(self):
|
|
self.active = False
|
|
self.halt()
|
|
self.get_display_surface().set_clip(None)
|
|
|
|
def start_game(self):
|
|
"""
|
|
Turn off the title screen and either display the level select or start level one if level select is disabled. Set the
|
|
most recent time to None so the most recent high score stops blinking.
|
|
"""
|
|
self.deactivate()
|
|
self.get_game().most_recent_score = None
|
|
if self.get_configuration("system", "enable-level-select"):
|
|
self.get_game().level_select.activate()
|
|
else:
|
|
self.get_game().level_select.launch(0)
|
|
|
|
def draw_score_to_column(self, score, column, screen_pos, rank):
|
|
"""
|
|
Blit `score` onto `column`, taking positioning and rank into account
|
|
|
|
@param score Score to display in top scores
|
|
@param column Surface for displaying score
|
|
@param screen_pos absolute screen (x, y) of score topleft
|
|
@param rank rank of score
|
|
@return height of the drawn score
|
|
"""
|
|
# Parse both strings and score objects
|
|
if isinstance(score, NS.Score):
|
|
text = score.formatted()
|
|
else:
|
|
text = score
|
|
|
|
# The background is based on rank
|
|
if rank == 0:
|
|
bg = 255, 215, 0
|
|
elif rank == 1:
|
|
bg = 192, 192, 192
|
|
elif rank == 2:
|
|
bg = 205, 127, 50
|
|
else:
|
|
bg = 255, 255, 255
|
|
|
|
# Draw the score
|
|
score_surface = render_box(self.score_font, text, width=column.get_width(), background=bg)
|
|
column.blit(score_surface, (0, screen_pos[1]))
|
|
|
|
# Create a blinking indicator for the most recent score
|
|
if score == self.get_game().most_recent_score:
|
|
self.score_indicator = BlinkingSprite(self, 500)
|
|
arrow = pygame.surface.Surface([score_surface.get_height()] * 2)
|
|
arrow.set_colorkey((255, 0, 255))
|
|
arrow.fill((255, 0, 255))
|
|
if screen_pos[0] == 0:
|
|
points = 0, arrow.get_height() // 2, arrow.get_width() - 1, 0, arrow.get_width() - 1, arrow.get_height() - 1
|
|
else:
|
|
points = 0, 0, 0, arrow.get_height() - 1, arrow.get_width() - 1, arrow.get_height() // 2
|
|
pygame.gfxdraw.filled_trigon(arrow, points[0], points[1], points[2], points[3], points[4], points[5], bg)
|
|
pygame.gfxdraw.trigon(arrow, points[0], points[1], points[2], points[3], points[4], points[5], (0, 0, 0))
|
|
self.score_indicator.add_frame(arrow)
|
|
self.score_indicator.location.top = screen_pos[1]
|
|
if screen_pos[0] == 0:
|
|
self.score_indicator.location.left = score_surface.get_width() + 5
|
|
else:
|
|
self.score_indicator.location.right = screen_pos[0] - 5
|
|
|
|
# The height is used to move the draw location
|
|
return score_surface.get_height()
|
|
|
|
def draw_heading_to_column(self, text, column, y):
|
|
"""
|
|
Blit `text` on `column` in the heading style
|
|
|
|
@param text heading text to blit
|
|
@param column Surface where heading will be blit
|
|
@param y top position of text
|
|
@return height of drawn heading
|
|
"""
|
|
heading = render_box(self.heading_font, text, color=(255, 255, 255), background=(0, 0, 0), width=column.get_width())
|
|
column.blit(heading, (0, y))
|
|
return heading.get_height()
|
|
|
|
def draw_scores(self):
|
|
"""
|
|
Create two columns, one for each side of the screen. Draw as many scores as can fit along each column, in order from
|
|
best to worst, separating them evenly into categories: normal, advanced, and expert. Save the columns as sprites. Note
|
|
that this doesn't support non-level select mode anymore.
|
|
"""
|
|
ds = self.get_display_surface()
|
|
self.score_indicator = None
|
|
heading_width, heading_height = self.heading_font.size("ADVANCED")
|
|
heading_width += 10
|
|
score_height = self.score_font.size("0")[1]
|
|
column_width, column_height = heading_width, ds.get_height()
|
|
left_score_count = (column_height - heading_height * 2) // score_height
|
|
right_score_count = (column_height - heading_height) // score_height
|
|
total_score_count = left_score_count + right_score_count
|
|
per_category_count, remainder = divmod(total_score_count, 3)
|
|
left_column_sprite = Sprite(self)
|
|
left_column = pygame.surface.Surface((column_width, column_height))
|
|
left_column.fill((255, 255, 255))
|
|
x, y = 0, 0
|
|
y += self.draw_heading_to_column("NORMAL", left_column, y)
|
|
count = per_category_count
|
|
if remainder:
|
|
count += 1
|
|
remainder -= 1
|
|
for rank, score in enumerate(sorted([score for score in self.get_game().scores if score.level_index == 0])[:count]):
|
|
y += self.draw_score_to_column(score, left_column, (x, y), rank)
|
|
left_score_count -= 1
|
|
y += self.draw_heading_to_column("ADVANCED", left_column, y)
|
|
count = per_category_count
|
|
if remainder:
|
|
count += 1
|
|
remainder -= 1
|
|
right_column_sprite = Sprite(self)
|
|
right_column = pygame.surface.Surface((column_width, column_height))
|
|
right_column.fill((255, 255, 255))
|
|
column = left_column
|
|
for rank, score in enumerate(sorted([score for score in self.get_game().scores if score.level_index == 1])[:count]):
|
|
y += self.draw_score_to_column(score, column, (x, y), rank)
|
|
if left_score_count == 1:
|
|
y = 0
|
|
x = ds.get_width() - column_width
|
|
column = right_column
|
|
left_column_sprite.add_frame(left_column)
|
|
if left_score_count == 0:
|
|
right_score_count -= 1
|
|
else:
|
|
left_score_count -= 1
|
|
y += self.draw_heading_to_column("EXPERT", right_column, y)
|
|
count = per_category_count
|
|
for rank, score in enumerate(sorted([score for score in self.get_game().scores if score.level_index == 2])[:count]):
|
|
y += self.draw_score_to_column(score, right_column, (x, y), rank)
|
|
right_column_sprite.add_frame(right_column)
|
|
right_column_sprite.location.topleft = x, 0
|
|
if not self.get_configuration("system", "optimize-title-screen") and self.get_configuration("display", "scores-alpha") < 255:
|
|
alpha = self.get_configuration("display", "scores-alpha")
|
|
left_column.set_alpha(alpha)
|
|
right_column.set_alpha(alpha)
|
|
self.score_sprites = [left_column_sprite, right_column_sprite]
|
|
|
|
def show_video(self):
|
|
self.video.unhide()
|
|
self.play(self.hide_video, delay=self.get_configuration("time", "attract-gif-length"), play_once=True)
|
|
self.get_game().tony.set_frameset("static")
|
|
self.unlock_index = 0
|
|
self.get_game().platform.set_glowing(self.get_game().platform.get_buttons_from_edges([self.UNLOCK_MOVES[0]]))
|
|
|
|
def hide_video(self):
|
|
self.video.hide()
|
|
self.play(self.show_video, delay=self.get_configuration("time", "attract-board-length"), play_once=True)
|
|
self.get_game().tony.set_frameset("board")
|
|
|
|
def update(self):
|
|
"""
|
|
Scroll the background, check for button presses for the unlock pattern, handle switching between attract mode
|
|
with the GIFs active and unlocking pattern mode, and draw the screen
|
|
"""
|
|
Animation.update(self)
|
|
if self.active:
|
|
ds = self.get_display_surface()
|
|
dsr = ds.get_rect()
|
|
|
|
# Draw the background
|
|
self.get_game().logo.update()
|
|
|
|
# Advance through the unlock pattern
|
|
platform = self.get_game().platform
|
|
if not self.get_game().wipe.is_playing() and platform.get_edge_pressed() == self.UNLOCK_MOVES[self.unlock_index]:
|
|
if self.unlock_index == len(self.UNLOCK_MOVES) - 1:
|
|
platform.set_glowing([])
|
|
self.get_game().wipe.start(self.start_game)
|
|
self.get_audio().play_sfx("confirm")
|
|
else:
|
|
self.unlock_index += 1
|
|
platform.set_glowing(platform.get_buttons_from_edges([self.UNLOCK_MOVES[self.unlock_index]]))
|
|
self.get_audio().play_sfx("land_0")
|
|
self.get_game().tony.update()
|
|
|
|
# Draw the scores unless optimized out
|
|
if not self.get_configuration("system", "optimize-title-screen"):
|
|
for sprite in self.score_sprites:
|
|
sprite.update()
|
|
|
|
# Bounce the GIF around the screen
|
|
if self.video.location.right > dsr.right or self.video.location.left < dsr.left:
|
|
self.angle = reflect_angle(self.angle, 0)
|
|
if self.video.location.right > dsr.right:
|
|
self.video.move(dsr.right - self.video.location.right)
|
|
else:
|
|
self.video.move(dsr.left - self.video.location.left)
|
|
if self.video.location.bottom > dsr.bottom or self.video.location.top < dsr.top:
|
|
self.angle = reflect_angle(self.angle, pi)
|
|
if self.video.location.bottom > dsr.bottom:
|
|
self.video.move(dy=dsr.bottom - self.video.location.bottom)
|
|
else:
|
|
self.video.move(dy=dsr.top - self.video.location.top)
|
|
dx, dy = get_delta(self.angle, 5, False)
|
|
self.video.move(dx, dy)
|
|
|
|
# Hide GIFs/attract mode (or keep them hidden) if input is detected. Set a countdown that will turn
|
|
# attract mode back on if no input is detected before the countdown expires. As long as input keeps
|
|
# being detected, this block will keep running and restarting the countdown.
|
|
if platform.get_pressed():
|
|
self.video.hide()
|
|
self.get_game().tony.set_frameset("static")
|
|
self.halt()
|
|
self.play(self.show_video, delay=self.get_configuration("time", "attract-reset-countdown"), play_once=True)
|
|
|
|
# Indicate most recent score
|
|
if self.score_indicator is not None:
|
|
self.score_indicator.update()
|
|
|
|
class Dialogue(Animation):
|
|
"""
|
|
This class creates the graphics for displaying character dialog. It displays an avatar, a character name, and a box with the
|
|
dialog text in classic RPG format. It uses the Animation class to scroll the text onto the screen with a sound effect to mimic
|
|
talking.
|
|
"""
|
|
|
|
BACKGROUND = 255, 255, 255
|
|
BORDER = 0, 0, 0
|
|
TEXT_COLOR = 0, 0, 0
|
|
FONT_PATH = "rounded-mplus-1m-bold.ttf"
|
|
FONT_SIZE = 18
|
|
|
|
def __init__(self, parent):
|
|
Animation.__init__(self, parent)
|
|
ds = self.get_display_surface()
|
|
dsr = ds.get_rect()
|
|
frame = Surface((dsr.w, 72))
|
|
frame.fill(self.BORDER)
|
|
frame.fill(self.BACKGROUND, (1, 1, frame.get_width() - 2, frame.get_height() - 2))
|
|
self.text_box = Sprite(self)
|
|
self.text_box.add_frame(frame)
|
|
self.text_box.location.bottomleft = dsr.bottomleft
|
|
frame = Surface((66, 66))
|
|
frame.fill(self.BORDER)
|
|
frame.fill(self.BACKGROUND, (1, 1, frame.get_width() - 2, frame.get_height() - 2))
|
|
self.avatar_box = Sprite(self)
|
|
self.avatar_box.add_frame(frame)
|
|
self.avatar_box.location.bottomleft = self.text_box.location.topleft
|
|
frame = Surface((128, 24))
|
|
frame.fill(self.BORDER)
|
|
frame.fill(self.BACKGROUND, (1, 1, frame.get_width() - 2, frame.get_height() - 2))
|
|
self.name_box = Sprite(self)
|
|
self.name_box.add_frame(frame)
|
|
self.name_box.location.bottomleft = self.avatar_box.location.bottomright
|
|
self.speech_channel = None
|
|
self.base_message_frame = Surface((self.text_box.location.w - 10, 30 * 2))
|
|
self.base_message_frame.fill(self.BACKGROUND)
|
|
|
|
def reset(self):
|
|
self.stop_speech()
|
|
self.halt()
|
|
self.deactivate()
|
|
self.first_pressed = False
|
|
self.first_press_elapsed = 0
|
|
|
|
def stop_speech(self):
|
|
if self.speech_channel is not None:
|
|
self.speech_channel.stop()
|
|
self.speech_channel = None
|
|
|
|
def deactivate(self):
|
|
self.stop_speech()
|
|
self.active = False
|
|
|
|
def activate(self):
|
|
self.active = True
|
|
|
|
def set_avatar(self, image):
|
|
self.avatar = Sprite(self)
|
|
self.avatar.add_frame(image)
|
|
self.avatar.location.center = self.avatar_box.location.center
|
|
|
|
def set_name(self, text):
|
|
font = pygame.font.Font(self.get_resource(self.FONT_PATH), self.FONT_SIZE)
|
|
self.name = Sprite(self)
|
|
self.name.add_frame(font.render(text, True, self.TEXT_COLOR).convert_alpha())
|
|
self.name.location.midleft = self.name_box.location.left + 5, self.name_box.location.centery
|
|
|
|
def show_text(self, text):
|
|
self.full_text = text
|
|
self.text_index = 0
|
|
self.speech_channel = self.get_audio().play_sfx("talk", -1)
|
|
self.play()
|
|
|
|
def build_frame(self):
|
|
self.text_index += 2
|
|
if self.text_index >= len(self.full_text):
|
|
self.show_all()
|
|
|
|
def show_all(self):
|
|
self.stop_speech()
|
|
self.text_index = len(self.full_text)
|
|
self.halt()
|
|
|
|
def update(self):
|
|
if self.active:
|
|
Animation.update(self)
|
|
self.avatar_box.update()
|
|
self.avatar.update()
|
|
self.name_box.update()
|
|
self.name.update()
|
|
self.text_box.update()
|
|
font = pygame.font.Font(self.get_resource(self.FONT_PATH), self.FONT_SIZE)
|
|
message = Sprite(self)
|
|
lines = self.full_text[:self.text_index].split("\n")
|
|
frame = self.base_message_frame.copy()
|
|
for ii, line in enumerate(lines):
|
|
surface = font.render(line, True, self.TEXT_COLOR, self.BACKGROUND).convert()
|
|
frame.blit(surface, (0, 30 * ii))
|
|
message.add_frame(frame)
|
|
message.location.topleft = self.text_box.location.left + 9, self.text_box.location.top + 8
|
|
message.update()
|
|
|
|
|
|
class SkipPrompt(GameChild):
|
|
|
|
def __init__(self, parent, callback):
|
|
GameChild.__init__(self, parent)
|
|
self.callback = callback
|
|
self.buttons = []
|
|
self.pluses = []
|
|
top = 3
|
|
left = 3
|
|
for ii, edge in enumerate((NS.S, NS.NE, NS.W)):
|
|
self.buttons.append(Button(self, edge, AdvancePrompt.BUTTON_SIZE, AdvancePrompt.BUTTON_BORDER))
|
|
self.buttons[-1].location.topleft = left, top
|
|
if ii < 2:
|
|
self.pluses.append(Sprite(self))
|
|
self.pluses[-1].load_from_path(self.get_resource("Plus.png"), True)
|
|
self.pluses[-1].location.center = (
|
|
self.buttons[-1].location.right + AdvancePrompt.BUTTON_SPACING / 2,
|
|
self.buttons[-1].location.centery)
|
|
left += self.buttons[-1].location.width + AdvancePrompt.BUTTON_SPACING
|
|
self.text = Sprite(self)
|
|
font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), 18)
|
|
self.text.add_frame(font.render("TO SKIP", True, (0, 0, 0)).convert_alpha())
|
|
self.text.location.midleft = (
|
|
self.buttons[2].location.right + 5,
|
|
self.buttons[2].location.centery)
|
|
self.button_sound = self.get_audio().sfx["button"]
|
|
|
|
def reset(self):
|
|
self.press_index = 0
|
|
self.press_elapsed = 0
|
|
for button in self.buttons:
|
|
button.unhide()
|
|
for plus in self.pluses:
|
|
plus.unhide()
|
|
|
|
def update(self):
|
|
platform = self.get_game().platform
|
|
if self.press_index == 0 and platform.get_edge_pressed() == NS.S:
|
|
self.press_index += 1
|
|
self.button_sound.play()
|
|
self.buttons[0].hide()
|
|
self.pluses[0].hide()
|
|
elif self.press_index == 1 and platform.get_edge_pressed() == NS.NE:
|
|
self.press_index += 1
|
|
self.button_sound.play()
|
|
self.buttons[1].hide()
|
|
self.pluses[1].hide()
|
|
elif self.press_index == 2 and platform.get_edge_pressed() == NS.W:
|
|
self.callback()
|
|
self.get_audio().play_sfx("confirm")
|
|
elif self.press_index > 0:
|
|
self.press_elapsed += self.get_game().time_filter.get_last_frame_duration()
|
|
if self.press_elapsed > 4000:
|
|
self.reset()
|
|
for button in self.buttons:
|
|
button.update()
|
|
for plus in self.pluses:
|
|
plus.update()
|
|
self.text.update()
|
|
|
|
|
|
class AdvancePrompt(GameChild):
|
|
|
|
BUTTON_SIZE = 30
|
|
BUTTON_BORDER = 3
|
|
BUTTON_SPACING = 64
|
|
|
|
def __init__(self, parent):
|
|
GameChild.__init__(self, parent)
|
|
dsr = self.get_display_surface().get_rect()
|
|
self.buttons = Button(self, NS.N, self.BUTTON_SIZE, self.BUTTON_BORDER), \
|
|
Button(self, NS.NW, self.BUTTON_SIZE, self.BUTTON_BORDER)
|
|
self.plus = Sprite(self)
|
|
self.plus.load_from_path(self.get_resource("Plus.png"), True)
|
|
dsr = self.get_display_surface().get_rect()
|
|
self.plus.location.center = dsr.centerx, dsr.centery + 70
|
|
self.buttons[1].location.center = self.plus.location.move(self.BUTTON_SPACING, 0).center
|
|
self.buttons[0].location.center = self.plus.location.move(-self.BUTTON_SPACING, 0).center
|
|
self.background_rect = Rect(
|
|
self.buttons[0].location.topleft,
|
|
(self.buttons[1].location.right - self.buttons[0].location.left, self.buttons[0].location.height))
|
|
self.background_rect.inflate_ip((10, 10))
|
|
|
|
def reset(self):
|
|
self.cancel_first_press()
|
|
for button in self.buttons:
|
|
button.unhide()
|
|
self.plus.unhide()
|
|
|
|
def cancel_first_press(self):
|
|
self.first_pressed = False
|
|
self.first_pressed_elapsed = 0
|
|
self.buttons[0].unhide()
|
|
self.plus.unhide()
|
|
|
|
def check_first_press(self):
|
|
return not self.first_pressed and self.get_game().platform.get_edge_pressed() == NS.N
|
|
|
|
def press_first(self):
|
|
self.first_pressed = True
|
|
self.buttons[0].hide()
|
|
self.plus.hide()
|
|
self.get_audio().play_sfx("button")
|
|
|
|
def check_second_press(self):
|
|
pressed = self.first_pressed and self.get_game().platform.get_edge_pressed() == NS.NW
|
|
if pressed:
|
|
self.get_audio().play_sfx("confirm")
|
|
return pressed
|
|
|
|
def update(self):
|
|
if self.first_pressed:
|
|
self.first_pressed_elapsed += self.get_game().time_filter.get_last_frame_duration()
|
|
self.get_display_surface().fill((255, 255, 255), self.background_rect)
|
|
for button in self.buttons:
|
|
button.update()
|
|
self.plus.update()
|
|
|
|
|
|
class Wipe(Animation):
|
|
"""
|
|
This class creates a blinds screen wipe effect that can be given a callback function to be called exactly when the screen is
|
|
filled with the wipe graphic. This allows the game to transition between states behind the wipe graphic to create a curtain
|
|
effect.
|
|
"""
|
|
|
|
BLIND_COUNT = 4
|
|
SPEED = 6
|
|
|
|
def __init__(self, parent):
|
|
"""
|
|
Initialize the wipe image and sound effect
|
|
|
|
@param parent PGFW game object that instantiated the wipe
|
|
"""
|
|
Animation.__init__(self, parent)
|
|
self.image = load(self.get_resource("Ink.png")).convert()
|
|
self.sound = self.get_audio().sfx["wipe"]
|
|
self.callback_kwargs = {}
|
|
|
|
def reset(self):
|
|
"""
|
|
Deactivate and stop the animation
|
|
"""
|
|
self.deactivate()
|
|
self.halt()
|
|
|
|
def deactivate(self):
|
|
self.active = False
|
|
|
|
def activate(self):
|
|
self.active = True
|
|
|
|
def start(self, callback, **kwargs):
|
|
"""
|
|
Trigger the wipe animation to begin. The given callback function will be called when the screen is filled with the
|
|
wipe graphic.
|
|
|
|
@param callback function to be called when the wipe is covering the screen
|
|
"""
|
|
self.activate()
|
|
self.up = True
|
|
self.blind_height = self.get_display_surface().get_height() / self.BLIND_COUNT
|
|
self.callback = callback
|
|
self.callback_kwargs = kwargs
|
|
self.play()
|
|
self.sound.play()
|
|
|
|
def build_frame(self):
|
|
"""
|
|
This grows and shrinks the height of the blinds that control how much of the wipe graphic is currently displayed. It
|
|
will be called automatically every frame as long as the wipe's update method is being called.
|
|
"""
|
|
if self.up:
|
|
self.blind_height -= self.SPEED
|
|
if self.blind_height <= 0:
|
|
self.up = False
|
|
self.callback(**self.callback_kwargs)
|
|
else:
|
|
self.blind_height += self.SPEED
|
|
if self.blind_height >= self.get_display_surface().get_height() / self.BLIND_COUNT:
|
|
self.halt()
|
|
self.deactivate()
|
|
self.get_game().unsuppress_input()
|
|
|
|
def update(self):
|
|
"""
|
|
Use the blind height value and screen clipping to draw the screen wipe in the state indicated by the blind height. The
|
|
screen is clipped to rects based on the blind height, and only those rects will have the wipe graphic drawn. Other screen
|
|
areas will show what is being drawn behind the screen wipe.
|
|
"""
|
|
if self.active:
|
|
Animation.update(self)
|
|
ds = self.get_display_surface()
|
|
dsr = ds.get_rect()
|
|
|
|
# Save the existing clip
|
|
existing_clip = ds.get_clip()
|
|
if existing_clip is not None:
|
|
left = existing_clip.left
|
|
width = existing_clip.width
|
|
else:
|
|
left = 0
|
|
width = dsr.w
|
|
|
|
# Draw blinds
|
|
for y in range(0, dsr.h, dsr.h // self.BLIND_COUNT):
|
|
if self.up:
|
|
ds.set_clip((left, y, width, dsr.h // self.BLIND_COUNT - self.blind_height))
|
|
else:
|
|
ds.set_clip((left, y + self.blind_height, width, dsr.h // self.BLIND_COUNT - self.blind_height))
|
|
ds.blit(self.image, (0, 0))
|
|
|
|
# Restore clip
|
|
ds.set_clip(existing_clip)
|
|
|
|
|
|
class Platform(GameChild):
|
|
"""
|
|
This class contains methods for manipulating and getting information about the platform the player is standing on,
|
|
both the real one and on-screen representation. It initializes four Light objects, one for each pad on the platform.
|
|
It can set lights to glowing, return the states of individual lights or pairs of lights, reset lights, draw the
|
|
on-screen representation, and track how long an edge has been continuously pressed.
|
|
"""
|
|
|
|
def __init__(self, parent, center):
|
|
"""
|
|
Initialize four lights, one for each pad on the platform. Initialize a Sprite for the pad graphics with one
|
|
frameset per six possible combinations of lights.
|
|
|
|
@param parent PGFW game object that initialized this object
|
|
@param center tuple that gives the (x, y) screen coordinates of this platform
|
|
"""
|
|
GameChild.__init__(self, parent)
|
|
|
|
# Create objects for tracking the individual pad lights
|
|
self.lights = [
|
|
Light(self, self.get_configuration("pads", "nw_color"), NS.LNW),
|
|
Light(self, self.get_configuration("pads", "ne_color"), NS.LNE),
|
|
Light(self, self.get_configuration("pads", "se_color"), NS.LSE),
|
|
Light(self, self.get_configuration("pads", "sw_color"), NS.LSW)
|
|
]
|
|
|
|
# Create a sprite which will display the pad
|
|
self.view = Sprite(self)
|
|
self.view.load_from_path(os.path.join(self.get_resource("pad"), "pad_0.png"), True)
|
|
self.view.add_frameset([0], name="neutral")
|
|
|
|
# For each of the six combinations of lights, create a glowing frameset
|
|
light_masks = load_frames(self.get_resource("pad_mask"), True)
|
|
for orientation_index, orientation in enumerate((NS.N, NS.E, NS.NW, NS.NE, NS.W, NS.S)):
|
|
pad_base_frame = load_frames(os.path.join(self.get_resource("pad"), f"pad_{orientation_index + 1}.png"), True)[0]
|
|
self.view.add_frame(pad_base_frame)
|
|
frameset = [len(self.view.frames) - 1]
|
|
intensity_resolution = 12
|
|
for intensity in range(1, intensity_resolution):
|
|
next_pad_frame = pad_base_frame.copy()
|
|
for light_orientation in self.get_buttons_from_edges([orientation]):
|
|
copy = light_masks[light_orientation].copy()
|
|
pixels = pygame.PixelArray(copy)
|
|
color = pygame.Color(0, 0, 0)
|
|
h, s, l, a = color.hsla
|
|
l = int(intensity / intensity_resolution * 100)
|
|
color.hsla = h, s, l, a
|
|
pixels.replace(pygame.Color(0, 0, 0), color)
|
|
del pixels
|
|
next_pad_frame.blit(copy, (0, 0), None, BLEND_RGBA_ADD)
|
|
self.view.add_frame(next_pad_frame)
|
|
frameset.append(len(self.view.frames) - 1)
|
|
self.view.add_frameset(frameset, name=str(orientation))
|
|
self.view.location.center = center
|
|
|
|
def reset(self):
|
|
"""
|
|
Deactivate this object and reset each light. Reset press elapsed tracker.
|
|
"""
|
|
self.deactivate()
|
|
self.reset_lights()
|
|
self.previously_pressed_edge = None
|
|
self.press_elapsed = 0
|
|
|
|
def reset_lights(self):
|
|
for light in self.lights:
|
|
light.reset()
|
|
|
|
def deactivate(self):
|
|
"""
|
|
This will stop the platform from being drawn and lights from updating
|
|
"""
|
|
self.active = False
|
|
|
|
def activate(self):
|
|
"""
|
|
This will cause the platform to get drawn and lights to update when this object's update method is called
|
|
"""
|
|
self.active = True
|
|
|
|
def unpress(self):
|
|
"""
|
|
Set the state of each light to unpressed
|
|
"""
|
|
for light in self.lights:
|
|
light.pressed = False
|
|
|
|
def get_pressed(self):
|
|
"""
|
|
Returns a list of light positions pressed (NS.LNW, NS.LNE, NS.LSE, NS.LSW)
|
|
"""
|
|
return [light.position for light in self.lights if light.pressed]
|
|
|
|
def get_edge_pressed(self):
|
|
"""
|
|
Gets the edge (2 light combination) currently pressed. This only returns one edge since there should only
|
|
be one able to be pressed at a time. If no edge is pressed, returns None.
|
|
|
|
@return NS.N | NS.NE | NS.E | NS.NW | NS.S | NS.W | None
|
|
"""
|
|
pressed = self.get_pressed()
|
|
if NS.LNW in pressed and NS.LNE in pressed:
|
|
return NS.N
|
|
elif NS.LNE in pressed and NS.LSW in pressed:
|
|
return NS.NE
|
|
elif NS.LNE in pressed and NS.LSE in pressed:
|
|
return NS.E
|
|
elif NS.LNW in pressed and NS.LSE in pressed:
|
|
return NS.NW
|
|
elif NS.LSE in pressed and NS.LSW in pressed:
|
|
return NS.S
|
|
elif NS.LSW in pressed and NS.LNW in pressed:
|
|
return NS.W
|
|
|
|
def get_glowing_edge(self):
|
|
"""
|
|
Return the edge currently glowing or None
|
|
|
|
@return NS.N | NS.NE | NS.E | NS.NW | NS.S | NS.W | None
|
|
"""
|
|
if self.lights[NS.LNW].glowing() and self.lights[NS.LNE].glowing():
|
|
return NS.N
|
|
elif self.lights[NS.LNE].glowing() and self.lights[NS.LSW].glowing():
|
|
return NS.NE
|
|
elif self.lights[NS.LNE].glowing() and self.lights[NS.LSE].glowing():
|
|
return NS.E
|
|
elif self.lights[NS.LNW].glowing() and self.lights[NS.LSE].glowing():
|
|
return NS.NW
|
|
elif self.lights[NS.LSE].glowing() and self.lights[NS.LSW].glowing():
|
|
return NS.S
|
|
elif self.lights[NS.LSW].glowing() and self.lights[NS.LNW].glowing():
|
|
return NS.W
|
|
|
|
def get_buttons_from_edges(self, edges):
|
|
"""
|
|
Get a list of light positions contained by a list of edges. For example, [NS.N, NS.E] would give [NS.LNW, NS.LNE, NS.LSE].
|
|
|
|
@param edges list of edges [NS.N | NS.NE | NS.E | NS.NW | NS.S | NS.W]
|
|
@return list of light positions [NS.LNW | NS.LNE | NS.LSE | NS.LSW]
|
|
"""
|
|
buttons = set()
|
|
for edge in edges:
|
|
if edge == NS.N:
|
|
buttons = buttons.union((NS.LNW, NS.LNE))
|
|
elif edge == NS.NE:
|
|
buttons = buttons.union((NS.LNE, NS.LSW))
|
|
elif edge == NS.E:
|
|
buttons = buttons.union((NS.LNE, NS.LSE))
|
|
elif edge == NS.NW:
|
|
buttons = buttons.union((NS.LNW, NS.LSE))
|
|
elif edge == NS.S:
|
|
buttons = buttons.union((NS.LSE, NS.LSW))
|
|
elif edge == NS.W:
|
|
buttons = buttons.union((NS.LSW, NS.LNW))
|
|
return list(buttons)
|
|
|
|
def get_steps_from_edge(self, edge):
|
|
"""
|
|
Get the edges that are one step away from a given edge. For example, NS.N would give (NS.NE, NS.NW) because those
|
|
are the edges that only require a pivot move of one step from NS.N.
|
|
|
|
@param edge one of NS.N, NS.NE, NS.E, NS.NW, NS.S, NS.W
|
|
@return pair of edges that are one step away
|
|
"""
|
|
if edge == NS.N:
|
|
return NS.NE, NS.NW
|
|
elif edge == NS.NE:
|
|
return NS.N, NS.E, NS.S, NS.W
|
|
elif edge == NS.E:
|
|
return NS.NE, NS.NW
|
|
elif edge == NS.NW:
|
|
return NS.N, NS.E, NS.S, NS.W
|
|
elif edge == NS.S:
|
|
return NS.NE, NS.NW
|
|
elif edge == NS.W:
|
|
return NS.NE, NS.NW
|
|
|
|
def get_right_angles_from_edge(self, edge):
|
|
"""
|
|
Get the pair of angles that are at a right angle to a given edge. For example, NS.N would return (NS.E, NW.W). For
|
|
diagonals, this returns None.
|
|
|
|
@param edge one of NS.N, NS.NE, NS.E, NS.NW, NS.S, NS.W
|
|
@return pair of edges that are at a right angle to given edge or None
|
|
"""
|
|
if edge == NS.N:
|
|
return NS.E, NS.W
|
|
elif edge == NS.NE:
|
|
return None
|
|
elif edge == NS.E:
|
|
return NS.N, NS.S
|
|
elif edge == NS.NW:
|
|
return None
|
|
elif edge == NS.S:
|
|
return NS.E, NS.W
|
|
elif edge == NS.W:
|
|
return NS.N, NS.S
|
|
|
|
def get_opposite_of_edge(self, edge):
|
|
"""
|
|
Get the edge opposite to a given edge. For example, NS.N would return NS.S. For diagonals, the opposite is the
|
|
reverse diagonal.
|
|
|
|
@param edge one of NS.N, NS.NE, NS.E, NS.NW, NS.S, NS.W
|
|
@return edge opposite to given edge, one of NS.N, NS.NE, NS.E, NS.NW, NS.S, NS.W
|
|
"""
|
|
if edge == NS.N:
|
|
return NS.S
|
|
elif edge == NS.NE:
|
|
return NS.NW
|
|
elif edge == NS.E:
|
|
return NS.W
|
|
elif edge == NS.NW:
|
|
return NS.NE
|
|
elif edge == NS.S:
|
|
return NS.N
|
|
elif edge == NS.W:
|
|
return NS.E
|
|
|
|
def get_color_pair_from_edge(self, edge):
|
|
"""
|
|
Return the pair of pygame color objects that make up a given edge
|
|
|
|
@param edge one of NS.N, NS.NE, NS.E, NS.NW, NS.S, NS.W
|
|
@return tuple of pygame color objects
|
|
"""
|
|
if edge == NS.N:
|
|
return self.lights[NS.LNW].color, self.lights[NS.LNE].color
|
|
elif edge == NS.NE:
|
|
return self.lights[NS.LNE].color, self.lights[NS.LSW].color
|
|
elif edge == NS.E:
|
|
return self.lights[NS.LNE].color, self.lights[NS.LSE].color
|
|
elif edge == NS.NW:
|
|
return self.lights[NS.LNW].color, self.lights[NS.LSE].color
|
|
elif edge == NS.S:
|
|
return self.lights[NS.LSW].color, self.lights[NS.LSE].color
|
|
elif edge == NS.W:
|
|
return self.lights[NS.LNW].color, self.lights[NS.LSW].color
|
|
|
|
def set_glowing(self, selected):
|
|
"""
|
|
Set the given light IDs to glowing and other indices to not glowing.
|
|
|
|
@param selected list of light IDs (NS.LNW, NS.LNE, NS.LSE, NS.LSW)
|
|
"""
|
|
for ii, light in enumerate(self.lights):
|
|
light.glow_index = 0
|
|
light.unglow()
|
|
if ii in selected:
|
|
light.glow()
|
|
|
|
def update(self):
|
|
"""
|
|
Update each light and draw the platform and glow effect
|
|
"""
|
|
if self.active:
|
|
for light in self.lights:
|
|
light.update()
|
|
# draw the pad based on which pads are glowing
|
|
glowing = self.get_glowing_edge()
|
|
if glowing is None:
|
|
self.view.set_frameset("neutral")
|
|
self.view.update()
|
|
else:
|
|
self.view.set_frameset(str(glowing))
|
|
self.view.update()
|
|
# track how long an edge has been pressed
|
|
if self.get_edge_pressed() is not None:
|
|
if self.get_edge_pressed() != self.previously_pressed_edge:
|
|
self.previously_pressed_edge = self.get_edge_pressed()
|
|
self.press_elapsed = 0
|
|
else:
|
|
self.press_elapsed += self.get_game().time_filter.get_last_frame_duration()
|
|
else:
|
|
self.previously_pressed_edge = None
|
|
self.press_elapsed = 0
|
|
|
|
|
|
class Light(GameChild):
|
|
"""
|
|
This class represents a pad on the platform. Typically there are four instances for a platform, one for each corner of the
|
|
platform. Each light stores its color, position on the platform, and state of glowing.
|
|
"""
|
|
|
|
TITLE_OFFSET = 0
|
|
|
|
def __init__(self, parent, color, position):
|
|
"""
|
|
Initialize a new Light object, providing color and position on the platform.
|
|
|
|
@param parent PGFW game object that instantiated this object
|
|
@param color pygame color object
|
|
@param position the light's position on the platform, one of NS.LNW, NS.LNE, NS.LSE, NS.LSW
|
|
"""
|
|
GameChild.__init__(self, parent)
|
|
self.color = Color(color)
|
|
self.color.a = 225
|
|
self.position = position
|
|
self.pressed = False
|
|
ds = self.get_display_surface()
|
|
frontleft = ds.get_width() / 2 - NS.FRONT_WIDTH / 2, NS.FRONT
|
|
backleft = ds.get_width() / 2 - NS.BACK_WIDTH / 2, NS.FRONT + NS.LENGTH
|
|
left_step = get_step_relative(frontleft, backleft, NS.STEP)
|
|
midleft = frontleft[0] + left_step[0], frontleft[1] + left_step[1]
|
|
frontmid = ds.get_width() / 2, NS.FRONT
|
|
mid = ds.get_width() / 2, NS.FRONT + NS.LENGTH * NS.STEP
|
|
backmid = ds.get_width() / 2, NS.FRONT + NS.LENGTH
|
|
frontright = ds.get_width() / 2 + NS.FRONT_WIDTH / 2, NS.FRONT
|
|
backright = ds.get_width() / 2 + NS.BACK_WIDTH / 2, NS.FRONT + NS.LENGTH
|
|
right_step = get_step_relative(frontright, backright, NS.STEP)
|
|
midright = frontright[0] + right_step[0], frontright[1] + right_step[1]
|
|
if self.position == NS.LNW:
|
|
self.points = frontleft, frontmid, mid, midleft
|
|
elif self.position == NS.LNE:
|
|
self.points = frontmid, frontright, midright, mid
|
|
elif self.position == NS.LSE:
|
|
self.points = mid, midright, backright, backmid
|
|
elif self.position == NS.LSW:
|
|
self.points = midleft, mid, backmid, backleft
|
|
|
|
def reset(self):
|
|
"""
|
|
Unhide, halt glow animation
|
|
"""
|
|
self.unglow()
|
|
|
|
def glow(self):
|
|
"""
|
|
Set the glow state to True
|
|
"""
|
|
self.is_glowing = True
|
|
|
|
def unglow(self):
|
|
"""
|
|
Set the glow state to False
|
|
"""
|
|
self.is_glowing = False
|
|
|
|
def glowing(self):
|
|
"""
|
|
Returns True if this light is glowing, False otherwise
|
|
|
|
@return True | False
|
|
"""
|
|
return self.is_glowing
|
|
|
|
def update(self):
|
|
"""
|
|
Checks the attack state to determine whether to start or stop glowing
|
|
"""
|
|
if not self.get_game().title.active and not self.get_game().level_select.active:
|
|
boss = self.get_game().boss
|
|
chemtrails = self.get_game().chemtrails
|
|
# checks the boss attack queue and chameleon queue index to see if the glow should be started now
|
|
if boss.queue and self.in_orientation(boss.queue[chemtrails.queue_index]):
|
|
self.glow()
|
|
# turns off the glow
|
|
elif not boss.queue or not self.in_orientation(boss.queue[chemtrails.queue_index]):
|
|
self.unglow()
|
|
|
|
def get_points(self):
|
|
if self.get_game().title.active:
|
|
points = []
|
|
for point in self.points:
|
|
points.append((point[0], point[1] - self.TITLE_OFFSET))
|
|
return points
|
|
else:
|
|
return self.points
|
|
|
|
def in_orientation(self, orientation):
|
|
"""
|
|
Returns True if this light is contained in the given edge
|
|
|
|
@param orientation edge to check, one of NS.N, NS.NW, NS.W, NS.NE, NS.E, NS.S
|
|
@return True | False
|
|
"""
|
|
if self.position == NS.LNW:
|
|
return orientation in (NS.N, NS.NW, NS.W)
|
|
elif self.position == NS.LNE:
|
|
return orientation in (NS.N, NS.NE, NS.E)
|
|
elif self.position == NS.LSE:
|
|
return orientation in (NS.NW, NS.E, NS.S)
|
|
elif self.position == NS.LSW:
|
|
return orientation in (NS.S, NS.NE, NS.W)
|
|
|
|
|
|
class Chemtrails(Sprite):
|
|
"""
|
|
This class stores the graphics and state of the player character. It contains sprite frames, health and life objects, and the
|
|
timer that counts down the amount of time left to perform a move.
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
"""
|
|
Load the sprite frames, one for each pad orientation. Initialize a health object, lives object, and timer. Create a sprite
|
|
for the tongue.
|
|
|
|
@param parent PGFW game object that initialized this object
|
|
"""
|
|
Sprite.__init__(self, parent, framerate=125)
|
|
for directory in sorted(iglob(join(self.get_resource("littleSlimeGoop"), "[0-9]_*/"))):
|
|
self.add_frameset(switch=True)
|
|
self.load_from_path(directory, True)
|
|
self.add_frameset(name="hurt", switch=True)
|
|
self.load_from_path("littleSlimeGoop/Hurt", True)
|
|
self.tongue = Sprite(self, 160)
|
|
self.tongue.load_from_path("littleSlimeGoop/justTongue", True)
|
|
self.set_frameset(NS.N)
|
|
self.register(self.cancel_hurt)
|
|
self.life = Life(self)
|
|
self.boys = Boys(self)
|
|
self.timer = Timer(self)
|
|
|
|
def reset(self):
|
|
"""
|
|
Reset the health, lives, and timer objects and deactivate.
|
|
"""
|
|
self.deactivate()
|
|
self.life.reset()
|
|
self.boys.reset()
|
|
self.timer.reset()
|
|
self.set_frameset(NS.N)
|
|
|
|
def deactivate(self):
|
|
self.active = False
|
|
|
|
def activate(self):
|
|
self.active = True
|
|
|
|
def challenge(self):
|
|
"""
|
|
Start an attempt against a new queue of swords to be cleared.
|
|
"""
|
|
self.timer.reset()
|
|
self.queue_index = 0
|
|
|
|
def display_hurt(self):
|
|
"""
|
|
Show hurt animation and trigger it to end after a delay
|
|
"""
|
|
self.set_frameset("hurt")
|
|
self.play(self.cancel_hurt, delay=self.get_configuration("time", "lizard-hurt-length"), play_once=True)
|
|
|
|
def cancel_hurt(self):
|
|
"""
|
|
Reset to a non-hurt frameset
|
|
"""
|
|
self.set_frameset(NS.N)
|
|
self.orient()
|
|
|
|
def attack(self):
|
|
"""
|
|
Hit the boss if this is called while the boss attack queue is active and the player is in the correct orientation.
|
|
Add time to the timer, decrease the boss's health, and play a sound effect. If the queue is finished, reset the
|
|
timer and pass control back to the boss to end the combo.
|
|
"""
|
|
boss = self.get_game().boss
|
|
queue = boss.queue
|
|
if self.orientation == queue[self.queue_index]:
|
|
self.blem()
|
|
self.timer.add_time(self.get_configuration("time", f"timer-addition-level-{boss.level_index + 1}"))
|
|
boss.health.decrease(self.get_configuration("boss", f"damage-per-hit-level-{boss.level_index + 1}"))
|
|
self.queue_index += 1
|
|
boss.last_attack = self.orientation
|
|
boss.sword.block()
|
|
if self.queue_index == len(queue):
|
|
self.timer.reset()
|
|
boss.end_combo()
|
|
else:
|
|
self.get_audio().play_sfx("land_0")
|
|
self.get_game().platform.reset_lights()
|
|
|
|
def orient(self):
|
|
"""
|
|
Place the sprite on screen based on which edge is being pressed by the player on the real mat.
|
|
"""
|
|
ds = self.get_display_surface()
|
|
edge = self.get_game().platform.get_edge_pressed()
|
|
dy = -Light.TITLE_OFFSET if self.get_game().title.active else 0
|
|
if edge is not None:
|
|
if self.get_current_frameset().name != "hurt":
|
|
self.set_frameset(edge + 1)
|
|
self.unhide()
|
|
else:
|
|
self.hide()
|
|
if edge == NS.N:
|
|
self.location.center = ds.get_width() / 2, NS.FRONT + dy - 10
|
|
self.orientation = NS.N
|
|
elif edge == NS.E:
|
|
self.location.center = ds.get_width() / 2 + NS.FRONT_WIDTH / 2 - 85, NS.FRONT + NS.LENGTH * NS.STEP - 40 + dy
|
|
self.orientation = NS.E
|
|
elif edge == NS.S:
|
|
self.location.center = ds.get_width() / 2, NS.FRONT + NS.LENGTH - NS.LENGTH * NS.STEP - 65 + dy
|
|
self.orientation = NS.S
|
|
elif edge == NS.W:
|
|
self.location.center = ds.get_width() / 2 - NS.FRONT_WIDTH / 2 + 85, NS.FRONT + NS.LENGTH * NS.STEP - 40 + dy
|
|
self.orientation = NS.W
|
|
elif edge == NS.NW:
|
|
self.location.center = ds.get_width() / 2, NS.FRONT + NS.LENGTH * NS.STEP + dy - 45
|
|
self.orientation = NS.NW
|
|
elif edge == NS.NE:
|
|
self.location.center = ds.get_width() / 2 - 5, NS.FRONT + NS.LENGTH * NS.STEP - 45 + dy
|
|
self.orientation = NS.NE
|
|
else:
|
|
self.orientation = None
|
|
|
|
def blem(self):
|
|
"""
|
|
Start the tongue animation to block the sword
|
|
"""
|
|
if self.orientation in (NS.N, NS.NE):
|
|
self.tongue.location.center = self.location.centerx + 10, self.location.top
|
|
elif self.orientation == NS.E:
|
|
self.tongue.location.center = self.location.right - 10, self.location.top + 25
|
|
elif self.orientation == NS.NW:
|
|
self.tongue.location.center = self.location.centerx, self.location.top + 27
|
|
elif self.orientation == NS.S:
|
|
self.tongue.location.center = self.location.centerx, self.location.top + 100
|
|
elif self.orientation == NS.W:
|
|
self.tongue.location.center = self.location.left + 13, self.location.top + 23
|
|
self.tongue.unhide()
|
|
self.tongue.get_current_frameset().reset()
|
|
|
|
def update(self, offset: Vector=(0, 0)):
|
|
if self.active:
|
|
self.orient()
|
|
self.location.move(offset)
|
|
# Draw tongue behind lizard if it the bottom, otherwise draw tongue in front of lizard
|
|
if self.orientation == NS.S:
|
|
self.tongue.update()
|
|
Sprite.update(self)
|
|
if self.orientation != NS.S:
|
|
self.tongue.update()
|
|
# End the tongue animation after one play
|
|
if self.tongue.get_current_frameset().current_index == len(self.tongue.get_current_frameset().order) - 1:
|
|
self.tongue.hide()
|
|
if not self.get_game().title.active and not self.get_game().level_select.active:
|
|
boss = self.get_game().boss
|
|
if boss.queue:
|
|
self.timer.tick()
|
|
self.attack()
|
|
if self.timer.amount < 0:
|
|
self.life.decrease()
|
|
if not boss.is_playing(boss.show_end_dialogue, include_delay=True):
|
|
self.timer.reset()
|
|
boss.combo()
|
|
if not boss.is_playing(boss.show_introduction_dialogue, include_delay=True):
|
|
self.timer.update()
|
|
self.life.update()
|
|
# self.boys.update()
|
|
|
|
|
|
class Timer(Meter):
|
|
|
|
def __init__(self, parent):
|
|
Meter.__init__(self, parent)
|
|
dsr = self.get_display_surface().get_rect()
|
|
background = load(self.get_resource("HUD_timer.png")).convert()
|
|
rect = background.get_rect()
|
|
rect.bottomright = dsr.right - 4, dsr.bottom - 4
|
|
self.setup(background, rect, 53, (0, 0, 255),
|
|
self.get_configuration("time", "timer-start-level-1"), "scrapeIcons/scrapeIcons_07.png")
|
|
|
|
def reset(self):
|
|
"""
|
|
Account for the differences in time per level by setting a custom amount based on the boss's level
|
|
"""
|
|
super().reset()
|
|
# The difference between level 1 and the current level is how much to remove from the timer
|
|
difference = self.get_configuration("time", "timer-start-level-1") - \
|
|
self.get_configuration("time", f"timer-start-level-{self.get_game().boss.level_index + 1}")
|
|
self.change(-difference)
|
|
|
|
def add_time(self, amount):
|
|
self.change(amount)
|
|
|
|
def tick(self):
|
|
self.change(-self.get_game().time_filter.get_last_frame_duration())
|
|
|
|
|
|
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()
|
|
rect = background.get_rect()
|
|
rect.bottomleft = 4, dsr.bottom - 4
|
|
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)
|
|
self.get_game().boss.finish_battle(False)
|
|
|
|
|
|
class Boys(Meter):
|
|
|
|
def __init__(self, parent):
|
|
Meter.__init__(self, parent)
|
|
dsr = self.get_display_surface().get_rect()
|
|
background = load(self.get_resource("HUD_lives.png")).convert()
|
|
rect = background.get_rect()
|
|
rect.bottomleft = 6, dsr.bottom - 4
|
|
# The amount of lives depends on whether it's boss rush or level select mode.
|
|
if self.get_configuration("system", "enable-level-select"):
|
|
lives = self.get_configuration("system", "lives-level-select-mode")
|
|
else:
|
|
lives = self.get_configuration("system", "lives-boss-rush-mode")
|
|
self.setup(background, rect, 60, (0, 255, 0), lives, "scrapeIcons/scrapeIcons_01.png")
|
|
|
|
|
|
class BossSprite(Sprite):
|
|
"""
|
|
Overload the Sprite class to do custom animation for bosses
|
|
"""
|
|
|
|
def shift_frame(self):
|
|
"""
|
|
Customize the sprite animation to play the entrance animation only once and to loop over the last three frames
|
|
of the death animation.
|
|
"""
|
|
frameset = self.get_current_frameset()
|
|
if frameset.name == "entrance" and frameset.get_current_id() == frameset.order[-1]:
|
|
self.set_frameset("normal")
|
|
elif frameset.name == "death" and frameset.current_index in (-1, len(frameset.order) - 1):
|
|
frameset.current_index = -4
|
|
super().shift_frame()
|
|
|
|
|
|
class Boss(Animation):
|
|
"""
|
|
The Boss object also serves as the level object, and it is expected that only one of these objects is initialized.
|
|
Its drawing, animations, timings, etc will be determined by the level_index member variable. For example, if
|
|
level_index is 0, the kool man sprite will be drawn, but if level_index is 2, the spoopy sprite will be drawn.
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
"""
|
|
Load graphics for boss sprites, avatars, and backgrounds. Initialize boss health and swords objects. Register
|
|
animations that control attacks, effects, and dialog.
|
|
"""
|
|
Animation.__init__(self, parent)
|
|
self.battle_finished = False
|
|
# Set up sprites with boil, hit, and intro animations
|
|
self.boss_sprites = []
|
|
self.boss_sprite_arms = []
|
|
for path in (pathlib.Path(self.get_resource("koolAnimations")), pathlib.Path(self.get_resource("alienAnimations")),
|
|
pathlib.Path(self.get_resource("emoAnimations")), pathlib.Path(self.get_resource("silverAnimations"))):
|
|
prefix = path.stem.split("Animations")[0]
|
|
self.boss_sprites.append(BossSprite(self, 42))
|
|
self.boss_sprites[-1].add_frameset(name="normal", switch=True)
|
|
self.boss_sprites[-1].load_from_path(path.joinpath(f"{prefix}Boil"), True)
|
|
self.boss_sprites[-1].add_frameset(name="hurt", switch=True)
|
|
self.boss_sprites[-1].load_from_path(path.joinpath(f"{prefix}Hit"), True)
|
|
self.boss_sprites[-1].add_frameset(name="death", switch=True)
|
|
self.boss_sprites[-1].load_from_path(path.joinpath(f"{prefix}Death"), True)
|
|
self.boss_sprites[-1].add_frameset(name="entrance", switch=True)
|
|
self.boss_sprites[-1].load_from_path(path.joinpath(f"{prefix}Intro"), True)
|
|
self.boss_sprites[-1].location.topleft = 207, 10
|
|
# Set the arm to its own sprite
|
|
self.boss_sprite_arms.append(Sprite(self, 60))
|
|
# Map the strings used to indicate direction in the animations directory to the IDs defined in the script
|
|
name_map = {
|
|
"U": NS.N,
|
|
"DR": NS.NE,
|
|
"R": NS.E,
|
|
"DL": NS.NW,
|
|
"D": NS.S,
|
|
"L": NS.W,
|
|
}
|
|
# Set static frames for arms, one for each of the 6 board orientations
|
|
root = path.joinpath(f"{prefix}Arms/Moving")
|
|
static_arm_frame_map = {
|
|
"UtoDR/*05.png": NS.N,
|
|
"UtoDR/*10.png": NS.NE,
|
|
"RtoDL/*05.png": NS.E,
|
|
"RtoDL/*10.png": NS.NW,
|
|
"DtoL/*05.png": NS.S,
|
|
"DtoL/*10.png": NS.W
|
|
}
|
|
orientation_frame_indices = {}
|
|
for arm_frame_path, orientation in static_arm_frame_map.items():
|
|
base = pygame.image.load(str(list(root.glob(arm_frame_path))[0]))
|
|
colorkeyed = fill_colorkey(base)
|
|
self.boss_sprite_arms[-1].add_frame(colorkeyed)
|
|
frame_index = len(self.boss_sprite_arms[-1].frames) - 1
|
|
self.boss_sprite_arms[-1].add_frameset([frame_index], name=str(orientation))
|
|
orientation_frame_indices[orientation] = frame_index
|
|
# Add sword smear animations to the alien's arm, one for each of the 30 possible combinations of 6 board orientations
|
|
for directory in path.joinpath(f"{prefix}Arms/Moving").iterdir():
|
|
if directory.is_dir():
|
|
frame_paths = list(sorted(directory.iterdir()))
|
|
# Extract board orientation IDs from the directory name
|
|
orientation_1, orientation_2 = [name_map[orientation] for orientation in directory.name.split("to")]
|
|
# Alien arm sprite frame indices for each orientation
|
|
frame_index_orientation_1 = orientation_frame_indices[orientation_1]
|
|
frame_index_orientation_2 = orientation_frame_indices[orientation_2]
|
|
# Add orientation_1 -> orientation_2 animation
|
|
frame_order = [frame_index_orientation_1]
|
|
for frame_path in frame_paths[5:9]:
|
|
self.boss_sprite_arms[-1].load_from_path(frame_path, True)
|
|
frame_order.append(len(self.boss_sprite_arms[-1].frames) - 1)
|
|
frame_order.append(frame_index_orientation_2)
|
|
self.boss_sprite_arms[-1].add_frameset(frame_order, name=f"{orientation_1}_{orientation_2}")
|
|
# Add orientation_2 -> orientation_1 animation
|
|
frame_order = [frame_index_orientation_2]
|
|
for frame_path in frame_paths[0:4]:
|
|
self.boss_sprite_arms[-1].load_from_path(frame_path, True)
|
|
frame_order.append(len(self.boss_sprite_arms[-1].frames) - 1)
|
|
frame_order.append(frame_index_orientation_1)
|
|
self.boss_sprite_arms[-1].add_frameset(frame_order, name=f"{orientation_2}_{orientation_1}")
|
|
self.boss_sprite_arms[-1].location.center = self.boss_sprites[-1].location.center
|
|
self.boss_sprite_arms[-1].hide()
|
|
# Boss sprite aliases
|
|
self.kool_man, self.alien, self.spoopy, self.metal = self.boss_sprites
|
|
self.kool_man_arms, self.alien_arms, self.spoopy_arms, self.metal_arms = self.boss_sprite_arms
|
|
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.end_player_damage, self.end_hit_animation, self.warning, self.enter_boss)
|
|
self.register(self.flash_player_damage, interval=100)
|
|
self.kool_man_avatar = load(self.get_resource("Kool_man_avatar.png")).convert()
|
|
self.alien_avatar = load(self.get_resource("Alien_avatar.png")).convert()
|
|
self.spoopy_avatar = load(self.get_resource("Spoopy_avatar.png")).convert()
|
|
self.metal_avatar = load(self.get_resource("Silver_avatar.png")).convert()
|
|
self.advance_prompt = AdvancePrompt(self)
|
|
self.backgrounds = [Sprite(self), Sprite(self), Sprite(self), Sprite(self)]
|
|
# 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")
|
|
# Inverted background
|
|
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)
|
|
# Darkened background
|
|
background.add_frameset(name="charging", switch=True)
|
|
frame = background.frames[0].copy()
|
|
frame.fill((80, 80, 80), None, pygame.BLEND_RGB_SUB)
|
|
background.add_frame(frame)
|
|
# Red background
|
|
background.add_frameset(name="warning", switch=True)
|
|
frame = background.frames[0].copy()
|
|
frame.fill((0, 150, 150), None, pygame.BLEND_RGB_SUB)
|
|
background.add_frame(frame)
|
|
# Shining background
|
|
background.add_frameset(name="shining", switch=True, framerate=120)
|
|
for hue in range(0, 360, 40):
|
|
frame = background.frames[0].copy()
|
|
color = Color(0, 0, 0)
|
|
color.hsla = hue, 30, 30, 100
|
|
frame.fill(color, None, pygame.BLEND_RGB_ADD)
|
|
background.add_frame(frame)
|
|
background.set_frameset("normal")
|
|
self.countdown = Countdown(self)
|
|
|
|
def cancel_flash(self):
|
|
pass
|
|
|
|
def start_level(self, index: int):
|
|
self.level_index = index
|
|
self.battle_finished = False
|
|
self.player_defeated = False
|
|
self.combo_count: int = 0
|
|
self.combo_rotations_save: int | None = None
|
|
self.health.reset()
|
|
self.get_game().chemtrails.timer.reset()
|
|
self.get_game().chemtrails.life.reset()
|
|
self.activate()
|
|
dialogue = self.get_game().dialogue
|
|
dialogue.deactivate()
|
|
if index == 0:
|
|
dialogue.set_avatar(self.kool_man_avatar)
|
|
dialogue.set_name("Kool Man")
|
|
self.kool_man.hide()
|
|
elif index == 1:
|
|
dialogue.set_avatar(self.alien_avatar)
|
|
dialogue.set_name("Alien")
|
|
self.alien.hide()
|
|
elif index == 2:
|
|
dialogue.set_avatar(self.spoopy_avatar)
|
|
dialogue.set_name("Spoopy")
|
|
self.spoopy.hide()
|
|
elif index == 3:
|
|
dialogue.set_avatar(self.metal_avatar)
|
|
dialogue.set_name("Silver")
|
|
self.metal.hide()
|
|
self.play(self.enter_boss, play_once=True, delay=1500)
|
|
self.get_audio().play_bgm(f"level_{index}")
|
|
self.play(self.show_introduction_dialogue, delay=3000, play_once=True)
|
|
self.get_game().platform.activate()
|
|
self.get_game().chemtrails.activate()
|
|
self.last_attack = NS.NW
|
|
self.backgrounds[self.level_index].set_frameset("normal")
|
|
self.halt(self.flash_player_damage)
|
|
|
|
def show_introduction_dialogue(self):
|
|
dialogue = self.get_game().dialogue
|
|
dialogue.activate()
|
|
if self.level_index == 0:
|
|
dialogue.show_text("You'll never be able to block my sword, you lizard slime!" +
|
|
" See if you can keep up\nwith these moves!")
|
|
elif self.level_index == 1:
|
|
dialogue.show_text("We're just warming up, slime breath! Prepare to get spun" +
|
|
" by these combos!")
|
|
elif self.level_index == 2:
|
|
dialogue.show_text("Lizard! My moves are so unpredictable you might as well" +
|
|
" give up now!")
|
|
elif self.level_index == 3:
|
|
dialogue.show_text("Get a clue, bottom feeder! My first move tells you all " +
|
|
"you need to know!")
|
|
self.play(self.end_dialogue, delay=5000, play_once=True)
|
|
|
|
def reset(self):
|
|
self.level_index = 0
|
|
self.kills = 0
|
|
self.time_elapsed = 0
|
|
self.deactivate()
|
|
self.cancel_flash()
|
|
self.halt(self.cancel_flash)
|
|
self.health.reset()
|
|
self.halt(self.brandish)
|
|
self.sword.reset()
|
|
self.advance_prompt.reset()
|
|
self.queue = None
|
|
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)
|
|
self.halt(self.warning)
|
|
|
|
def deactivate(self):
|
|
self.active = False
|
|
|
|
def activate(self):
|
|
self.active = True
|
|
|
|
def combo(self, delay=None):
|
|
"""
|
|
Trigger the boss to do a combo after a delay.
|
|
"""
|
|
if delay is None:
|
|
delay = self.get_configuration("boss", f"cooldown-level-{self.level_index + 1}")
|
|
self.queue = None
|
|
if self.get_game().serial_enabled():
|
|
self.get_game().reset_arduino()
|
|
self.play(self.brandish, delay=delay, play_once=True)
|
|
|
|
def brandish(self):
|
|
"""
|
|
Fill the queue with moves to be performed by the player.
|
|
"""
|
|
self.queue = []
|
|
platform = self.get_game().platform
|
|
choice = random.choice
|
|
if self.level_index == 0:
|
|
if self.health.amount > 90:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first]
|
|
elif self.health.amount > 70:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, choice(platform.get_steps_from_edge(first))]
|
|
elif self.health.amount > 30:
|
|
choices = [0]
|
|
if self.last_attack in (NS.NE, NS.NW):
|
|
choices.append(1)
|
|
else:
|
|
choices.extend((2, 3))
|
|
result = choice(choices)
|
|
if result == 0:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second, first, second]
|
|
elif result == 1:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, choice(platform.get_steps_from_edge(first)),
|
|
choice(platform.get_right_angles_from_edge(first))]
|
|
elif result == 2:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, choice(platform.get_steps_from_edge(first)),
|
|
platform.get_opposite_of_edge(first)]
|
|
elif result == 3:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second,
|
|
choice(platform.get_right_angles_from_edge(second))]
|
|
else:
|
|
choices = [0, 1]
|
|
if self.last_attack in (NS.NE, NS.NW):
|
|
choices.extend((2, 3, 4))
|
|
else:
|
|
choices.append(5)
|
|
result = choice(choices)
|
|
if result == 0 or result == 1:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
last = second if result else platform.get_opposite_of_edge(second)
|
|
self.queue = [first, second, platform.get_opposite_of_edge(first),
|
|
last]
|
|
elif result == 2:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, choice(platform.get_right_angles_from_edge(first)),
|
|
platform.get_opposite_of_edge(first)]
|
|
elif result == 3:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, choice(platform.get_steps_from_edge(first)),
|
|
choice(platform.get_right_angles_from_edge(first)),
|
|
platform.get_opposite_of_edge(first)]
|
|
elif result == 4:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second,
|
|
choice(platform.get_right_angles_from_edge(first)),
|
|
platform.get_opposite_of_edge(second)]
|
|
elif result == 5:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second, platform.get_opposite_of_edge(first),
|
|
choice(platform.get_right_angles_from_edge(second))]
|
|
elif self.level_index == 1:
|
|
if self.health.amount > 85:
|
|
if self.last_attack in (NS.NE, NS.NW):
|
|
choices = 1, 2
|
|
else:
|
|
choices = 0,
|
|
result = choice(choices)
|
|
if result == 0:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, platform.get_opposite_of_edge(first)]
|
|
elif result == 1:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, choice(platform.get_right_angles_from_edge(first)),
|
|
platform.get_opposite_of_edge(first)]
|
|
elif result == 2:
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
self.queue = [first, platform.get_opposite_of_edge(first)]
|
|
elif self.health.amount > 60:
|
|
if self.last_attack in (NS.NE, NS.NW):
|
|
choices = 2, 3
|
|
else:
|
|
choices = 0, 1
|
|
result = choice(choices)
|
|
first = choice(platform.get_steps_from_edge(self.last_attack))
|
|
if result == 0:
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second, platform.get_opposite_of_edge(second)]
|
|
elif result == 1:
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second,
|
|
choice(platform.get_right_angles_from_edge(second)),
|
|
platform.get_opposite_of_edge(first)]
|
|
elif result == 2:
|
|
second = platform.get_opposite_of_edge(first)
|
|
self.queue = [first, second,
|
|
choice(platform.get_right_angles_from_edge(second))]
|
|
elif result == 3:
|
|
second = choice(platform.get_right_angles_from_edge(first))
|
|
self.queue = [first, second, platform.get_opposite_of_edge(first),
|
|
platform.get_opposite_of_edge(second)]
|
|
elif self.health.amount > 30:
|
|
result = choice(range(3))
|
|
if result == 0:
|
|
first = self.choose_new_edge((NS.N, NS.E, NS.S, NS.W))
|
|
self.queue = [first, choice(platform.get_steps_from_edge(first)),
|
|
platform.get_opposite_of_edge(first), first]
|
|
elif result == 1:
|
|
first = self.choose_new_edge((NS.NE, NS.NW))
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second, platform.get_opposite_of_edge(second),
|
|
choice(platform.get_right_angles_from_edge(second))]
|
|
elif result == 2:
|
|
first = self.choose_new_edge((NS.NE, NS.NW))
|
|
second = choice(platform.get_steps_from_edge(first))
|
|
self.queue = [first, second,
|
|
choice(platform.get_right_angles_from_edge(second)),
|
|
platform.get_opposite_of_edge(second)]
|
|
else:
|
|
result = choice(range(4))
|
|
if result == 0:
|
|
first = self.choose_new_edge((NS.NE, NS.NW))
|
|
second = platform.get_opposite_of_edge(first)
|
|
self.queue = [first, second, first, second]
|
|
elif result == 1:
|
|
first = self.choose_new_edge((NS.N, NS.E, NS.S, NS.W))
|
|
self.queue = [first, platform.get_opposite_of_edge(first), first]
|
|
elif result == 2:
|
|
first = self.choose_new_edge((NS.N, NS.E, NS.S, NS.W))
|
|
self.queue = [first, choice(platform.get_steps_from_edge(first)),
|
|
choice(platform.get_right_angles_from_edge(first)),
|
|
platform.get_opposite_of_edge(first), first]
|
|
elif result == 3:
|
|
first = self.choose_new_edge((NS.N, NS.E, NS.S, NS.W))
|
|
second = platform.get_opposite_of_edge(first)
|
|
third = choice(platform.get_right_angles_from_edge(first))
|
|
self.queue = [first, second, third, platform.get_opposite_of_edge(second),
|
|
platform.get_opposite_of_edge(third)]
|
|
elif self.level_index == 2:
|
|
if self.health.amount > 90:
|
|
length = 4
|
|
elif self.health.amount > 70:
|
|
length = 5
|
|
elif self.health.amount > 40:
|
|
length = 6
|
|
else:
|
|
length = 8
|
|
while len(self.queue) < length:
|
|
while True:
|
|
orientation = random.randint(0, 5)
|
|
if (not self.queue and orientation != self.last_attack) or \
|
|
(len(self.queue) > 0 and orientation != self.queue[-1]):
|
|
self.queue.append(orientation)
|
|
break
|
|
|
|
# Level 4 has five combo sections. Each section must be completed before the next one is triggered. Each combo
|
|
# has the same shape but is rotated depending on which panel it randomly starts on.
|
|
elif self.level_index == 3:
|
|
|
|
# The first section is a two-step pattern from one side to the other and back, 3 times
|
|
if self.combo_count == 0:
|
|
choices = [
|
|
[NS.S, NS.NE, NS.N, NS.NW] * 3,
|
|
[NS.W, NS.NW, NS.E, NS.NE] * 3,
|
|
[NS.N, NS.NE, NS.S, NS.NW] * 3,
|
|
[NS.E, NS.NW, NS.W, NS.NE] * 3]
|
|
|
|
# The second section is a sweep to the left, back, to the right, back, then repeated
|
|
elif self.combo_count == 1:
|
|
choices = [
|
|
[NS.S, NS.NE, NS.W, NS.NE, NS.S, NS.NW, NS.E, NS.NW] * 2,
|
|
[NS.W, NS.NW, NS.N, NS.NW, NS.W, NS.NE, NS.S, NS.NE] * 2,
|
|
[NS.N, NS.NE, NS.E, NS.NE, NS.N, NS.NW, NS.W, NS.NW] * 2,
|
|
[NS.E, NS.NW, NS.S, NS.NW, NS.E, NS.NE, NS.N, NS.NE] * 2]
|
|
|
|
# The third section is a U twice and a slide to the opposite edge, then repeated twice.
|
|
elif self.combo_count == 2:
|
|
choices = [
|
|
[NS.S, NS.W, NS.S, NS.E, NS.S, NS.N] * 3,
|
|
[NS.W, NS.N, NS.W, NS.S, NS.W, NS.E] * 3,
|
|
[NS.N, NS.E, NS.N, NS.W, NS.N, NS.S] * 3,
|
|
[NS.E, NS.S, NS.E, NS.N, NS.E, NS.W] * 3]
|
|
|
|
# The fourth section is an edge, followed by double cross, performed four times, once for each edge
|
|
elif self.combo_count == 3:
|
|
choices = [
|
|
[NS.S, NS.NE, NS.NW, NS.NE, NS.NW, NS.N, NS.NW, NS.NE, NS.NW, NS.NE] * 2,
|
|
[NS.W, NS.NE, NS.NW, NS.NE, NS.NW, NS.E, NS.NW, NS.NE, NS.NW, NS.NE] * 2,
|
|
[NS.N, NS.NE, NS.NW, NS.NE, NS.NW, NS.S, NS.NW, NS.NE, NS.NW, NS.NE] * 2,
|
|
[NS.E, NS.NE, NS.NW, NS.NE, NS.NW, NS.W, NS.NW, NS.NE, NS.NW, NS.NE] * 2]
|
|
|
|
# The fifth section is two clockwise trips around the edges of the square, followed by slides back and forth
|
|
# across the platform -- then repeated once.
|
|
elif self.combo_count == 4:
|
|
# Define all four possible patterns
|
|
choices = [
|
|
[NS.N, NS.E, NS.S, NS.W, NS.N, NS.E, NS.S, NS.W, NS.N, NS.S, NS.N, NS.S] * 2,
|
|
[NS.E, NS.S, NS.W, NS.N, NS.E, NS.S, NS.W, NS.N, NS.E, NS.W, NS.E, NS.W] * 2,
|
|
[NS.S, NS.W, NS.N, NS.E, NS.S, NS.W, NS.N, NS.E, NS.S, NS.N, NS.S, NS.N] * 2,
|
|
[NS.W, NS.N, NS.E, NS.S, NS.W, NS.N, NS.E, NS.S, NS.W, NS.E, NS.W, NS.E] * 2]
|
|
|
|
# Set the queue to a random index in the pattern, making it start from one of the four sides randomly.
|
|
# Prevent it from being the last index on the first combo to make sure it doesn't start on the pad used for
|
|
# the level select.
|
|
#
|
|
# Only choose the index once per level and save it so the pattern is repeated exactly if the player fails.
|
|
if self.combo_rotations_save is None:
|
|
self.combo_rotations_save = random.randint(0, 2 if self.combo_count == 0 else 3)
|
|
self.queue = choices[self.combo_rotations_save]
|
|
|
|
self.unbrandished = copy(self.queue)
|
|
self.brandish_complete = False
|
|
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 sprite in self.boss_sprites:
|
|
sprite.set_frameset("normal")
|
|
|
|
def choose_new_edge(self, edges):
|
|
while True:
|
|
edge = random.choice(edges)
|
|
if edge != self.last_attack:
|
|
return edge
|
|
|
|
def end_combo(self):
|
|
"""
|
|
Start a new combo, unless the ending dialogue has been triggered. On level 4, reduce the boss's health to the
|
|
next milestone.
|
|
"""
|
|
# Play combo finish SFX
|
|
self.get_audio().play_sfx("complete_pattern_3")
|
|
|
|
# On level 4, every combo does a set amount damage (5 combos total = 20% each combo). Each hit still does
|
|
# damage though, so check how much health is left before reducing it. Remove the rotation save because a new
|
|
# combo will be launched.
|
|
if self.level_index == 3:
|
|
self.combo_count += 1
|
|
self.combo_rotations_save = None
|
|
# This will kill the boss on the final combo
|
|
self.health.decrease(self.health.amount - (5 - self.combo_count) * 20)
|
|
|
|
# Launch another combo, unless the ending was triggered by the boss's death
|
|
if not self.is_playing(self.show_end_dialogue, include_delay=True):
|
|
self.combo()
|
|
|
|
def finish_battle(self, win):
|
|
self.battle_finished = True
|
|
self.halt(self.brandish)
|
|
self.halt(self.cancel_flash)
|
|
self.halt(self.warning)
|
|
self.halt(self.flash_player_damage)
|
|
self.halt(self.end_player_damage)
|
|
self.sword.reset()
|
|
self.queue = []
|
|
self.brandish_complete = True
|
|
if win:
|
|
self.level_sprite().set_frameset("death")
|
|
if self.get_configuration("system", "enable-level-select"):
|
|
self.get_game().add_time_to_scores(self.time_elapsed, self.level_index)
|
|
elif self.level_index == 2:
|
|
if not self.get_configuration("system", "enable-level-select"):
|
|
self.get_game().add_time_to_scores(self.time_elapsed)
|
|
self.backgrounds[self.level_index].set_frameset("shining")
|
|
else:
|
|
self.level_sprite().set_frameset("normal")
|
|
self.play(self.flash_player_damage)
|
|
self.get_game().chemtrails.set_frameset("hurt")
|
|
|
|
# Record a play to the DNF file for analytics
|
|
try:
|
|
with open(self.get_configuration("system", "dnf-file"), "at") as dnf_file:
|
|
dnf_file.write(f"{self.time_elapsed} {self.level_index} {datetime.datetime.isoformat(datetime.datetime.now(), 'T')}\n")
|
|
except:
|
|
print("Error saving DNF run to file")
|
|
|
|
self.player_defeated = not win
|
|
self.kills += not win
|
|
self.play(self.show_end_dialogue, delay=3000, play_once=True)
|
|
|
|
def show_end_dialogue(self):
|
|
dialogue = self.get_game().dialogue
|
|
dialogue.activate()
|
|
self.get_audio().play_sfx("die")
|
|
if self.level_index == 0:
|
|
if self.player_defeated:
|
|
dialogue.show_text("Maybe next time!")
|
|
else:
|
|
dialogue.show_text("Hey! Wow! Lizard!")
|
|
elif self.level_index == 1:
|
|
if self.player_defeated:
|
|
dialogue.show_text("Wiped out!")
|
|
else:
|
|
if self.get_configuration("system", "enable-level-select"):
|
|
dialogue.show_text("Ouch! Oof!")
|
|
else:
|
|
dialogue.show_text("Well done! But it's not over yet!")
|
|
elif self.level_index == 2:
|
|
if self.player_defeated:
|
|
dialogue.show_text("Just like I thought!")
|
|
else:
|
|
if self.get_configuration("system", "enable-level-select"):
|
|
dialogue.show_text("H-how? But you're only a lizard!")
|
|
else:
|
|
dialogue.show_text("H-how? But you're only a lizard! How could you" +
|
|
" manage to defeat all of us?")
|
|
elif self.level_index == 3:
|
|
if self.player_defeated:
|
|
dialogue.show_text("Too slow!")
|
|
else:
|
|
dialogue.show_text("You're stronger than you look!")
|
|
if self.player_defeated:
|
|
self.countdown.activate()
|
|
else:
|
|
self.play(self.end_dialogue, delay=5000, play_once=True)
|
|
|
|
def end_dialogue(self):
|
|
self.get_game().dialogue.deactivate()
|
|
if not self.battle_finished:
|
|
self.combo(self.get_configuration("boss", "first-combo-delay"))
|
|
else:
|
|
self.get_game().wipe.start(self.transition_after_battle)
|
|
|
|
def transition_after_battle(self):
|
|
"""
|
|
Determine whether to reset to title screen, relaunch the current level, launch the next level, or activate the ending object and
|
|
call the appropriate method.
|
|
"""
|
|
# If the player is out of lives, reset the game.
|
|
if self.get_game().chemtrails.boys.amount <= 0:
|
|
self.get_game().reset(True)
|
|
# Check if the ending screen should be activated.
|
|
elif not self.player_defeated and self.get_configuration("system", "enable-level-select") or self.level_index == 2:
|
|
defeated_level_index = self.level_index
|
|
game = self.get_game()
|
|
game.boss.reset()
|
|
game.chemtrails.reset()
|
|
game.platform.reset()
|
|
game.ending.activate(defeated_level_index)
|
|
else:
|
|
# Level index to launch is the current level if the player was defeated, otherwise it's the next level. If the player wasn't
|
|
# defeated, level select mode would have launched the game ending, so the next level will only be launched when boss rush
|
|
# mode is active.
|
|
index = self.level_index + (not self.player_defeated)
|
|
self.start_level(index)
|
|
|
|
def transition_to_title(self):
|
|
self.get_game().reset(True)
|
|
|
|
def damage(self):
|
|
pass
|
|
|
|
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
|
|
"""
|
|
if not self.battle_finished:
|
|
for boss in (self.kool_man, self.alien, self.spoopy):
|
|
boss.set_frameset("normal")
|
|
|
|
def warning(self):
|
|
"""
|
|
Use this method as an animation to create a warning flash of the background that flashes according to the
|
|
amount of time left in the player's timer object.
|
|
"""
|
|
time_left = self.get_game().chemtrails.timer.percent()
|
|
warning_threshold = self.get_configuration("time", f"timer-warning-start-{self.level_index + 1}")
|
|
background = self.backgrounds[self.level_index]
|
|
if time_left > warning_threshold:
|
|
background.set_frameset("normal")
|
|
self.halt(self.warning)
|
|
else:
|
|
if background.get_current_frameset().name == "normal":
|
|
background.set_frameset("warning")
|
|
self.play(self.warning, play_once=True, delay=50)
|
|
else:
|
|
background.set_frameset("normal")
|
|
self.play(self.warning, play_once=True, delay=time_left / warning_threshold * 400)
|
|
|
|
def enter_boss(self):
|
|
self.level_sprite().unhide()
|
|
self.level_sprite().get_current_frameset().reset()
|
|
self.level_sprite().set_frameset("entrance")
|
|
|
|
def level_sprite(self, level_index: int | None = None) -> Sprite:
|
|
"""
|
|
Return the boss sprite associated with the given level index. If level index is not given, use the value in
|
|
`self.level_index`.
|
|
|
|
@param level_index index of the level of the requested sprite
|
|
|
|
@return The boss sprite associated with the given level index
|
|
"""
|
|
if level_index is None:
|
|
level_index = self.level_index
|
|
if level_index == 0:
|
|
return self.kool_man
|
|
elif level_index == 1:
|
|
return self.alien
|
|
elif level_index == 2:
|
|
return self.spoopy
|
|
else:
|
|
return self.metal
|
|
|
|
def level_sprite_arm(self, level_index: int | None = None) -> Sprite:
|
|
"""
|
|
Return the boss arm sprite associated with the given index. If index is not given, use the value in `self.level_index`.
|
|
|
|
@param level_index index of the level of the requested sprite
|
|
"""
|
|
if level_index is None:
|
|
level_index = self.level_index
|
|
if level_index == 0:
|
|
return self.kool_man_arms
|
|
elif level_index == 1:
|
|
return self.alien_arms
|
|
elif level_index == 2:
|
|
return self.spoopy_arms
|
|
else:
|
|
return self.metal_arms
|
|
|
|
def update(self):
|
|
"""
|
|
Update graphics
|
|
"""
|
|
if self.active:
|
|
self.backgrounds[self.level_index].update()
|
|
dialogue = self.get_game().dialogue
|
|
# Handle the continue countdown or increase time elapsed if the continue screen
|
|
if self.countdown.active and dialogue.active and self.get_game().chemtrails.boys.amount > 0:
|
|
if self.advance_prompt.check_first_press():
|
|
self.advance_prompt.press_first()
|
|
elif self.advance_prompt.check_second_press():
|
|
self.countdown.deactivate()
|
|
if dialogue.is_playing():
|
|
dialogue.show_all()
|
|
else:
|
|
self.get_game().dialogue.deactivate()
|
|
if not self.battle_finished:
|
|
self.combo()
|
|
else:
|
|
self.get_game().wipe.start(self.transition_after_battle)
|
|
self.advance_prompt.cancel_first_press()
|
|
else:
|
|
self.time_elapsed += self.get_game().time_filter.get_last_frame_duration()
|
|
Animation.update(self)
|
|
# Update boss sprite
|
|
boss_sprite = self.level_sprite()
|
|
boss_sprite_arm = self.level_sprite_arm()
|
|
boss_sprite.update()
|
|
if self.brandish_complete:
|
|
if self.queue is not None:
|
|
# Draw ghosts of the upcoming moves fading more as the move goes futher back in the queue
|
|
boss_sprite_arm.unhide()
|
|
remaining_positions = list(reversed(self.queue[self.get_game().chemtrails.queue_index:]))
|
|
for ii, position in enumerate(remaining_positions):
|
|
alpha = int((ii + 1) / len(remaining_positions) * 255)
|
|
boss_sprite_arm.set_frameset(str(position))
|
|
boss_sprite_arm.get_current_frame().set_alpha(alpha)
|
|
boss_sprite_arm.update()
|
|
boss_sprite_arm.get_current_frame().set_alpha(255)
|
|
else:
|
|
boss_sprite_arm.update()
|
|
self.sword.update()
|
|
self.health.update()
|
|
self.countdown.update()
|
|
# Trigger the warning effect if time is running out
|
|
if self.get_game().chemtrails.life.amount > 0 and not self.is_playing(self.warning, include_delay=True) and \
|
|
self.get_game().chemtrails.timer.percent() <= self.get_configuration(
|
|
"time", f"timer-warning-start-{self.level_index + 1}"):
|
|
self.play(self.warning, play_once=True)
|
|
|
|
def update_dialogue(self):
|
|
if self.active:
|
|
dialogue = self.get_game().dialogue
|
|
if dialogue.active:
|
|
self.get_game().dialogue.update()
|
|
if self.countdown.active and self.get_game().chemtrails.boys.amount > 0:
|
|
self.advance_prompt.update()
|
|
|
|
|
|
class Countdown(GameChild):
|
|
|
|
def __init__(self, parent):
|
|
GameChild.__init__(self, parent)
|
|
dsr = self.get_display_surface().get_rect()
|
|
font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), 76)
|
|
self.heading = Sprite(self)
|
|
self.heading.add_frame(font.render("CONTINUE?", True, (0, 0, 0), (255, 255, 255)).convert_alpha())
|
|
self.heading.location.midtop = dsr.centerx, 50
|
|
self.game_over = Sprite(self)
|
|
self.game_over.add_frame(font.render("GAME OVER", True, (0, 0, 0), (255, 255, 255)).convert_alpha())
|
|
self.game_over.location.center = dsr.centerx, dsr.centery - 40
|
|
self.glyphs = []
|
|
for ii in range(10):
|
|
glyph = Sprite(self)
|
|
frame = Surface((140, 140))
|
|
frame.fill((255, 255, 255))
|
|
digits = font.render("%i" % ii, True, (0, 0, 0), (255, 255, 255)).convert_alpha()
|
|
rect = digits.get_rect()
|
|
rect.center = frame.get_rect().center
|
|
frame.blit(digits, rect)
|
|
glyph.add_frame(frame)
|
|
glyph.location.center = dsr.centerx, dsr.centery - 30
|
|
self.glyphs.append(glyph)
|
|
|
|
def reset(self):
|
|
self.deactivate()
|
|
|
|
def deactivate(self):
|
|
self.active = False
|
|
|
|
def activate(self):
|
|
self.remaining = 9999
|
|
self.active = True
|
|
|
|
def end_game(self):
|
|
self.get_game().reset(True)
|
|
|
|
def update(self):
|
|
if self.active:
|
|
if self.get_game().chemtrails.boys.amount > 0:
|
|
self.heading.update()
|
|
self.glyphs[int(self.remaining / 1000)].update()
|
|
if not self.get_game().wipe.is_playing():
|
|
if self.remaining <= 0:
|
|
self.get_game().wipe.start(self.end_game)
|
|
self.remaining = 0
|
|
else:
|
|
self.remaining -= self.get_game().time_filter.get_last_frame_duration()
|
|
|
|
|
|
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)
|
|
# 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/", "sword/", "sword/", "sword/":
|
|
# 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 fast load is requested, use a single frame
|
|
if self.get_configuration("system", "minimize-load-time"):
|
|
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):
|
|
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
|
|
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
|
|
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.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)
|
|
dsr = self.get_display_surface().get_rect()
|
|
sprite = Sprite(self)
|
|
# 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
|
|
if position == NS.W:
|
|
sprite.location.centerx = dsr.centerx - 100
|
|
else:
|
|
sprite.location.centerx = dsr.centerx + 100
|
|
elif position in (NS.N, NS.S):
|
|
sprite.location.centerx = dsr.centerx
|
|
if position == NS.N:
|
|
sprite.location.centery = dsr.centery - 150
|
|
else:
|
|
sprite.location.centery = dsr.centery + 20
|
|
else:
|
|
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
|
|
self.parent.level_sprite_arm().unhide()
|
|
self.parent.level_sprite_arm().set_frameset(str(position))
|
|
if len(self.parent.unbrandished) > 0:
|
|
self.play(self.swab, delay=self.get_configuration("time", "sword-delay") - 60 * 4, play_once=True, position=position)
|
|
|
|
def swab(self, position):
|
|
"""
|
|
Activate boss's sword swab animation
|
|
"""
|
|
self.parent.level_sprite_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")
|
|
self.parent.level_sprite_arm().hide()
|
|
|
|
def block(self):
|
|
"""
|
|
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 active_sprite(self):
|
|
"""
|
|
Get the sprite that is currently front of the queue (next to get hit)
|
|
"""
|
|
return self.sprites[self.active_sprite_index]
|
|
|
|
def update(self):
|
|
"""
|
|
Draw previously blocked swords and the boss's current move sword.
|
|
"""
|
|
Animation.update(self)
|
|
if self.sprites:
|
|
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.alien.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()
|
|
self.get_audio().play_sfx("damage", x=sprite.location.centerx)
|
|
self.get_game().chemtrails.display_hurt()
|
|
else:
|
|
center_save = sprite.location.center
|
|
sprite.set_frameset("explode")
|
|
sprite.location.center = center_save
|
|
self.get_audio().play_sfx("explode", x=sprite.location.centerx)
|
|
if self.parent.is_playing(self.parent.end_hit_animation, include_delay=True):
|
|
self.parent.halt(self.parent.end_hit_animation)
|
|
self.parent.level_sprite().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
|
|
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):
|
|
"""
|
|
Track the boss's health and display the meter
|
|
"""
|
|
|
|
OFFSET = 4
|
|
|
|
def __init__(self, parent):
|
|
Meter.__init__(self, parent)
|
|
dsr = self.get_display_surface().get_rect()
|
|
self.background = load(self.get_resource("HUD_boss.png")).convert()
|
|
self.rect = self.background.get_rect()
|
|
self.rect.midtop = dsr.centerx, self.OFFSET
|
|
|
|
def setup(self):
|
|
level_index = self.get_game().boss.level_index
|
|
if level_index == 0:
|
|
icon_index = 22
|
|
elif level_index == 1:
|
|
icon_index = 17
|
|
elif level_index == 2:
|
|
icon_index = 19
|
|
elif level_index == 3:
|
|
icon_index = 23
|
|
Meter.setup(self, self.background, self.rect, 52, (255, 0, 255), 100, "scrapeIcons/scrapeIcons_%i.png" % icon_index)
|
|
|
|
def reset(self):
|
|
self.setup()
|
|
Meter.reset(self)
|
|
|
|
def decrease(self, damage):
|
|
self.change(-damage)
|
|
self.parent.damage()
|
|
if self.amount <= 0:
|
|
self.amount = 0
|
|
self.get_audio().play_sfx("complete_pattern_1")
|
|
self.get_audio().play_sfx("defeat")
|
|
self.get_game().boss.finish_battle(True)
|
|
else:
|
|
self.parent.play(self.parent.cancel_flash, delay=1000, play_once=True)
|
|
|
|
|
|
class Ending(Animation):
|
|
"""
|
|
Scene for the end of a successful play session. The Tony and slime bag sprites will be displayed, and Tony will say something
|
|
to the player. The player's time will be displayed as a sprite that bounces around the screen.
|
|
"""
|
|
|
|
BOSS_RUSH_TEXT = "Wow! You vanquished all the goons and skated like a pro, slime bag." + \
|
|
" You made your\nfather proud today. I love you, child."
|
|
|
|
def __init__(self, parent):
|
|
Animation.__init__(self, parent)
|
|
self.slime_bag = Chemtrails(self)
|
|
self.tony_avatar = load(self.get_resource("Introduction_tony_avatar.png")).convert()
|
|
self.time_font = pygame.font.Font(self.get_resource("rounded-mplus-1m-bold.ttf"), 64)
|
|
self.rank_font = pygame.font.Font(self.get_resource("rounded-mplus-1m-bold.ttf"), 26)
|
|
self.register(self.start, self.start_wipe)
|
|
self.register(self.append_sword, interval=1500)
|
|
self.swords = []
|
|
|
|
def reset(self):
|
|
self.deactivate()
|
|
self.halt()
|
|
self.text_index = 0
|
|
self.angle = random.choice((pi / 4, 3 * pi / 4, 5 * pi / 4, 7 * pi / 4))
|
|
self.slime_bag.reset()
|
|
|
|
def deactivate(self):
|
|
self.active = False
|
|
self.slime_bag.deactivate()
|
|
|
|
def activate(self, level_index):
|
|
self.defeated_level_index = level_index
|
|
self.active = True
|
|
self.play(self.start, delay=3000, play_once=True)
|
|
foreground = get_boxed_surface(
|
|
self.time_font.render(str(self.get_game().most_recent_score), False, (180, 150, 20), (255, 255, 255)).convert_alpha(),
|
|
background=(255, 255, 255), padding=(38, 0))
|
|
if self.rank()[0] % 100 // 10 != 1:
|
|
if self.rank()[0] % 10 == 1:
|
|
ordinal = "ST"
|
|
elif self.rank()[0] % 10 == 2:
|
|
ordinal = "ND"
|
|
elif self.rank()[0] % 10 == 3:
|
|
ordinal = "RD"
|
|
else:
|
|
ordinal = "TH"
|
|
else:
|
|
ordinal = "TH"
|
|
rank = self.rank_font.render(f"{self.rank()[0]}{ordinal}", False, (180, 150, 20), (255, 255, 255))
|
|
rank = pygame.transform.rotate(rank, 90)
|
|
rank_rect = rank.get_rect()
|
|
rank_rect.midleft = foreground.get_rect().midleft
|
|
foreground.blit(rank, rank_rect)
|
|
dsr = self.get_display_surface().get_rect()
|
|
self.text = RainbowSprite(self, foreground, 180, 200)
|
|
self.text.location.midtop = dsr.centerx, 80
|
|
self.get_game().tony.set_frameset("static")
|
|
dialogue = self.get_game().dialogue
|
|
dialogue.activate()
|
|
dialogue.set_avatar(self.tony_avatar)
|
|
dialogue.set_name("???")
|
|
dialogue.show_text("")
|
|
self.play(self.start_wipe, delay=self.get_configuration("time", "ending-timeout"), play_once=True)
|
|
self.get_audio().play_bgm("end")
|
|
self.slime_bag.activate()
|
|
self.play(self.append_sword)
|
|
|
|
def rank(self):
|
|
"""
|
|
@return the rank of the currently displaying score as a tuple: (rank, total)
|
|
"""
|
|
rank = 0
|
|
level_scores = [score for score in self.get_game().scores if score.level_index == self.defeated_level_index and not score.blank()]
|
|
for score in sorted(level_scores):
|
|
rank += 1
|
|
if score == self.get_game().most_recent_score:
|
|
break
|
|
return rank, len(level_scores)
|
|
|
|
def start(self):
|
|
dialogue = self.get_game().dialogue
|
|
if self.get_configuration("system", "enable-level-select"):
|
|
|
|
# Create a message for versus mode
|
|
if len(self.get_game().level_select.opponents_at_launch) > 1:
|
|
|
|
# Check if any peers had a faster time
|
|
rank = 1
|
|
for peer in self.get_game().level_select.opponents_at_launch:
|
|
if peer.address != "localhost" and not peer.status == "playing" and peer.result is not None and \
|
|
peer.result < self.get_game().most_recent_score.milliseconds:
|
|
rank += 1
|
|
|
|
if rank == 1:
|
|
text = (f"Congratulations on winning the battle and getting #{self.rank()[0]} out of {self.rank()[1]} overall!\n"
|
|
"Well done, slime bag. ")
|
|
else:
|
|
total = len(self.get_game().level_select.opponents_at_launch)
|
|
text = (f"You were #{rank} out of {total} in the battle, but you vanquished my goon and finished\n"
|
|
f"#{self.rank()[0]} out of {self.rank()[1]} overall! Well done, slime bag. ")
|
|
|
|
# Create a message for single-player mode
|
|
else:
|
|
text = (f"You vanquished my goon and got the #{self.rank()[0]} rank out of {self.rank()[1]} overall!\n"
|
|
"Well done, slime bag.")
|
|
|
|
if self.defeated_level_index == 2:
|
|
dialogue.set_name("Tony")
|
|
text += "You made your father proud today. I love you child."
|
|
elif self.defeated_level_index == 1:
|
|
text += "Give expert mode a try next."
|
|
else:
|
|
text += "Give advanced mode a try next."
|
|
else:
|
|
text = self.BOSS_RUSH_TEXT
|
|
dialogue.set_name("Tony")
|
|
dialogue.show_text(text)
|
|
self.text_index = 0
|
|
|
|
def end_game(self):
|
|
self.deactivate()
|
|
self.get_game().reset(True)
|
|
|
|
def start_wipe(self):
|
|
self.get_game().wipe.start(self.end_game)
|
|
|
|
def append_sword(self):
|
|
"""
|
|
Add a sword to the list based on what button is pressed. Remove swords that are out of view.
|
|
"""
|
|
edge = self.get_game().platform.get_edge_pressed()
|
|
if edge is not None:
|
|
sprite = Sprite(self)
|
|
# Add frames from Boss->Sword storage
|
|
for frame in self.get_game().boss.sword.swords[0][edge]:
|
|
sprite.add_frame(frame)
|
|
if edge == NS.W:
|
|
sprite.location.midleft = self.slime_bag.location.midleft
|
|
elif edge == NS.E:
|
|
sprite.location.midright = self.slime_bag.location.midright
|
|
else:
|
|
sprite.location.center = self.slime_bag.location.center
|
|
self.swords.append(sprite)
|
|
outgoing = []
|
|
for sword in self.swords:
|
|
if sword.location.bottom < 0:
|
|
outgoing.append(sword)
|
|
for sword in outgoing:
|
|
self.swords.remove(sword)
|
|
|
|
def update(self):
|
|
if self.active:
|
|
Animation.update(self)
|
|
dialogue = self.get_game().dialogue
|
|
wipe = self.get_game().wipe
|
|
self.get_game().logo.update()
|
|
self.get_game().tony.update()
|
|
# Draw swords shot at Tony
|
|
for sword in self.swords:
|
|
sword.move(0, -5)
|
|
sword.update()
|
|
self.slime_bag.update(offset=(0, -30))
|
|
dsr = self.get_display_surface().get_rect()
|
|
# Bounce the time sprite around the screen
|
|
if self.text.location.right > dsr.right or self.text.location.left < dsr.left:
|
|
self.angle = reflect_angle(self.angle, 0)
|
|
if self.text.location.right > dsr.right:
|
|
self.text.move(dsr.right - self.text.location.right)
|
|
else:
|
|
self.text.move(dsr.left - self.text.location.left)
|
|
if self.text.location.bottom > self.get_game().dialogue.avatar_box.location.top or self.text.location.top < dsr.top:
|
|
self.angle = reflect_angle(self.angle, pi)
|
|
if self.text.location.top < dsr.top:
|
|
self.text.move(dy=dsr.top - self.text.location.top)
|
|
else:
|
|
self.text.move(dy=self.get_game().dialogue.avatar_box.location.top - self.text.location.bottom)
|
|
dx, dy = get_delta(self.angle, 5, False)
|
|
self.text.move(dx, dy)
|
|
self.text.update()
|
|
self.get_game().dialogue.update()
|