3752 lines
162 KiB
Python
3752 lines
162 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
|
|
|
|
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
|
|
)
|
|
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-addition-level-1", "timer-addition-level-2", "timer-addition-level-3", "sword-delay",
|
|
"attract-gif-length", "attract-board-length", "attract-reset-countdown", "level-select-reset-countdown",
|
|
"level-select-press-length", "ending-timeout", "lizard-hurt-length"
|
|
],
|
|
"float": "timer-warning-start"
|
|
},
|
|
"boss":
|
|
{
|
|
"float": [
|
|
"damage-per-hit-level-1", "damage-per-hit-level-2",
|
|
"damage-per-hit-level-3"
|
|
],
|
|
"int": [
|
|
"cooldown-level-1", "cooldown-level-2",
|
|
"cooldown-level-3", "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"
|
|
}
|
|
})
|
|
|
|
# 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).
|
|
"""
|
|
if not self.suppressing_input and event.type in (KEYDOWN, KEYUP):
|
|
if self.last_press <= get_ticks() - int(self.get_configuration("input", "buffer")):
|
|
pressed = True if event.type == KEYDOWN else False
|
|
lights = self.platform.lights
|
|
self.idle_elapsed = 0
|
|
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
|
|
elif event.key == K_z:
|
|
self.reset()
|
|
elif event.key == K_a:
|
|
self.reset_arduino()
|
|
elif event.type == KEYDOWN and event.key == K_n and pygame.key.get_mods() & (pygame.KMOD_CTRL | pygame.KMOD_SHIFT):
|
|
# Toggle visibility of network diagnostics menu
|
|
state = self.get_configuration("network", "diagnostics")
|
|
self.configuration.set("network", "diagnostics", not state)
|
|
self.pop_up(f"Network diagnostics visible: {not state}")
|
|
self.last_press = get_ticks()
|
|
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)
|
|
y = 250
|
|
indent = 10
|
|
dsr = self.get_display_surface().get_rect()
|
|
self.platforms = [Platform(self, (0, y)), Platform(self, (0, y)), Platform(self, (0, y))]
|
|
scale = .75
|
|
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()
|
|
# for corner in platform.glow_masks:
|
|
# for ii, frame in enumerate(corner):
|
|
# scaled = pygame.transform.smoothscale(frame, (int(frame.get_width() * scale), int(frame.get_height() * scale)))
|
|
# corner[ii] = scaled
|
|
self.platforms[0].view.location.left = dsr.left + indent
|
|
self.platforms[1].view.location.centerx = dsr.centerx
|
|
self.platforms[2].view.location.right = dsr.right - indent
|
|
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))
|
|
preview_rect = pygame.Rect(0, 0, dsr.w / 3 - 40, 160)
|
|
self.previews = []
|
|
font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), 18)
|
|
padding = 4
|
|
for level_index, text in enumerate(("1 KOOL", "2 ALIEN", "3 GOTH")):
|
|
self.previews.append(Sprite(self, 100))
|
|
text = font.render(text, 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[level_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(level_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[level_index].view.location.centerx, \
|
|
self.platforms[level_index].view.location.top - 12
|
|
self.reset()
|
|
|
|
def activate(self):
|
|
self.reset()
|
|
self.active = True
|
|
for platform in self.platforms:
|
|
platform.activate()
|
|
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 level_index in range(3):
|
|
self.platforms[level_index].view.halt(self.platforms[level_index].view.wipe_out)
|
|
self.previews[level_index].halt(self.previews[level_index].wipe_out)
|
|
self.platforms[level_index].view.reset()
|
|
self.previews[level_index].reset()
|
|
self.platforms[level_index].view.unhide()
|
|
self.previews[level_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):
|
|
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():
|
|
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
|
|
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 += 0.1
|
|
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 o |