added option to load the game quickly by turning off some effects

This commit is contained in:
frank 2022-01-28 02:16:07 -05:00
parent 80c06582be
commit b099c97871
3 changed files with 198 additions and 235 deletions

327
NS.py
View File

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
import argparse
from random import randint, choice, random
from math import pi
from copy import copy
from glob import iglob
from os.path import basename, join
from threading import Thread
from serial import Serial, SerialException
from serial.tools import list_ports
from time import sleep
from PIL import Image
@ -48,8 +47,22 @@ class NS(Game, Animation):
NO_RESET_TIMEOUT = 3000
def __init__(self):
# 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")
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")
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":
@ -63,15 +76,57 @@ class NS(Game, Animation):
},
"display":
{
"float": "attract-gif-alpha"
"float": "attract-gif-alpha",
"bool": "effects"
},
"system":
{
"bool": "minimize-load-time"
}
})
# 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)
# Print the configuration if requested on the command line
if arguments.show_config:
print(self.get_configuration())
# Initialize the serial reader and launch a thread for reading from the serial port
if self.serial_enabled():
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:
for port in list_ports.comports():
print(f"Detected serial port: {port.device}")
exit()
# Open the port specified by the configuration or command line if it is found. If the specified port is not found,
# open the first found serial port. If no serial ports are found, 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)
elif devices:
self.serial_reader = Serial(devices[0], timeout=.3)
else:
raise SerialException("No serial port devices were detected. Use --no-serial for keyboard-only mode.")
self.serial_kill = False
self.serial_data = 0
self.reset_arduino()
self.serial_thread = Thread(target=self.read_serial)
self.serial_thread.start()
Animation.__init__(self, self)
self.subscribe(self.respond, KEYDOWN)
self.subscribe(self.respond, KEYUP)
self.subscribe(self.respond)
# for bgm in self.audio.bgm.values():
# bgm.volume = 0.65
ds = self.get_display_surface()
self.background = Surface(ds.get_size())
self.background.fill((0, 0, 0))
@ -79,26 +134,11 @@ class NS(Game, Animation):
self.tony = Tony(self)
self.logo = Logo(self)
self.title = Title(self)
self.introduction = Introduction(self)
self.ending = Ending(self)
self.wipe = Wipe(self)
self.dialogue = Dialogue(self)
self.chemtrails = Chemtrails(self)
self.boss = Boss(self)
if self.serial_enabled():
self.serial_kill = False
self.serial_data = 0
self.serial_reader = Serial(self.get_configuration("input", "arduino-port"),
timeout=.3)
self.reset_arduino()
# for port in list_ports.comports():
# print port.device
# print "---"
# ports = list_ports.grep(self.get_configuration("input", "arduino-port"))
# for port in ports:
# print port.device
self.serial_thread = Thread(target=self.read_serial)
self.serial_thread.start()
self.last_press = get_ticks()
self.register(self.blink_score, interval=500)
self.play(self.blink_score)
@ -107,7 +147,7 @@ class NS(Game, Animation):
clear()
def serial_enabled(self):
return self.get_configuration("input", "serial") and not self.check_command_line("-no-serial")
return self.get_configuration("input", "serial")
def read_serial(self):
while not self.serial_kill:
@ -154,7 +194,7 @@ class NS(Game, Animation):
def apply_serial(self):
for ii, light in enumerate(self.platform.lights):
light.pressed = bool(self.serial_data & (2 ** ii))
# reset idle timer is a light is detected as pressed in serial data
# reset idle timer if a light is detected as pressed in serial data
if light.pressed:
self.idle_elapsed = 0
@ -165,7 +205,6 @@ class NS(Game, Animation):
self.title.reset()
if not leave_wipe_running:
self.wipe.reset()
self.introduction.reset()
self.ending.reset()
self.boss.reset()
self.chemtrails.reset()
@ -188,6 +227,18 @@ class NS(Game, Animation):
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
@ -215,8 +266,7 @@ class NS(Game, Animation):
last_frame_duration = self.time_filter.get_last_frame_duration()
if self.serial_enabled():
self.apply_serial()
if self.title.active or self.introduction.active or self.ending.active or \
self.dialogue.active:
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:
@ -226,7 +276,6 @@ class NS(Game, Animation):
self.reset_arduino()
self.no_reset_elapsed = 0
self.title.update()
self.introduction.update()
self.ending.update()
self.boss.update()
if not self.title.active:
@ -337,18 +386,19 @@ class Tony(Sprite):
self.board.load_from_path(self.get_resource("newTony/TonyArms"), True)
self.effect = Sprite(self)
dsr = self.get_display_surface().get_rect()
for offset in range(12):
w, h = dsr.w + 40, int(dsr.h * .65)
glow = Surface((w, h), SRCALPHA)
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", "effects"):
for offset in range(12):
w, h = dsr.w + 40, int(dsr.h * .65)
glow = Surface((w, h), SRCALPHA)
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)
self.effect.location.topleft = -20, int(dsr.h * .35)
self.add_frame(load(self.get_resource("Big_Tony.png")).convert_alpha())
self.load_from_path(self.get_resource("newTony/TonyShirtHead"), True)
@ -416,7 +466,8 @@ class Video(Sprite):
filled_circle(self.mask, rect.centerx, rect.centery, rect.centerx, (0, 0, 0, alpha))
filled_circle(self.mask, rect.centerx, rect.centery, rect.centerx - 2, (255, 255, 255, alpha))
self.add_frame(self.mask)
self.play()
if not self.get_configuration("system", "minimize-load-time"):
self.play()
def shift_frame(self):
Sprite.shift_frame(self)
@ -430,17 +481,14 @@ class Video(Sprite):
frame = smoothscale(
fromstring(self.gif.convert("RGBA").tobytes(), self.gif.size, "RGBA"),
(self.mask.get_width(), int(self.gif.width * self.gif.height / self.mask.get_width())))
# frame = scale(
# fromstring(self.gif.convert("RGBA").tobytes(), self.gif.size, "RGBA"),
# (self.mask.get_width(), int(self.gif.width * self.gif.height / self.mask.get_width())))
copy = self.mask.copy()
rect = frame.get_rect()
rect.bottom = copy.get_rect().bottom
copy.blit(frame, rect, None, BLEND_RGBA_MIN)
# copy.blit(frame, rect)
self.clear_frames()
self.add_frame(copy)
class Logo(Sprite):
def __init__(self, parent):
@ -708,180 +756,6 @@ class Dialogue(Animation):
message.update()
class Introduction(Animation):
TEXT = (
"Hey, you lizard slime bag. It's me Giant Tony. " + \
"Do you think you\ncan skate like me? Prove it!",
"I'll even give you my board for this adventure. And ink my name\n" + \
"on it. Now the power of Giant Tony pulses through you.",
# "Before you go, show me you can scrape! Use your board to touch\n" + \
"Before you play, show me you can scrape! Use your board to touch\n" + \
"the glowing pads on the platform!", \
# "Good job, lizard scum! Maybe now you're ready to take on Kool\n" + \
"Good job, slime bag! Maybe now you're ready to take on Kool\n" + \
"Man and his friends. Don't let me down!")
SKATEBOARD_START = -30, -20
TUTORIAL_MOVES = NS.S, NS.NE, NS.N, NS.E
def __init__(self, parent):
Animation.__init__(self, parent)
self.words = []
for word in "hey you lizard slime bag show me you can scrape".split(" "):
font = Font(self.get_resource(Dialogue.FONT_PATH), 96)
sprite = RainbowSprite(self, font.render(word, True, (255, 0, 0)).convert_alpha(), 30)
self.words.append(sprite)
self.skateboard = Sprite(self)
self.skateboard.load_from_path(self.get_resource("Introduction_skateboard.png"), True)
self.slime_bag = Sprite(self)
self.slime_bag.load_from_path(self.get_resource("Introduction_slime_bag.png"), True)
self.slime_bag.load_from_path(self.get_resource("Introduction_slime_bag_board.png"), True)
self.slime_bag.add_frameset([0], name="standing", switch=True)
self.slime_bag.add_frameset([1], name="board")
self.slime_bag.location.center = self.get_display_surface().get_rect().center
self.tony_avatar = load(self.get_resource("Introduction_tony_avatar.png")).convert()
self.advance_prompt = AdvancePrompt(self)
self.skip_prompt = SkipPrompt(self, self.start_wipe)
self.register(self.start, self.move_board, self.take_board, self.speak)
def reset(self):
self.deactivate()
self.slime_bag.set_frameset("standing")
self.slime_bag.unhide()
self.halt()
self.skateboard.hide()
self.text_index = 0
self.tutorial_index = 0
self.words_index = 0
self.advance_prompt.reset()
self.skip_prompt.reset()
for word in self.words:
word.location.center = self.get_display_surface().get_rect().centerx, 100
word.hide()
def deactivate(self):
self.active = False
def activate(self):
self.active = True
self.play(self.start, delay=3000, play_once=True)
self.words[0].unhide()
self.play(self.speak)
# self.get_game().platform.unpress()
def speak(self):
for ii in range(self.words_index + 1):
self.words[ii].move(0, 12)
if ii == self.words_index and self.words[ii].location.bottom > self.get_display_surface().get_rect().bottom - 40:
if self.words_index < len(self.words) - 1:
self.words_index += 1
self.words[self.words_index].unhide()
self.get_audio().play_sfx("talk")
def start(self):
self.advance_prompt.cancel_first_press()
dialogue = self.get_game().dialogue
dialogue.activate()
dialogue.set_avatar(self.tony_avatar)
dialogue.set_name("???")
# dialogue.show_text(self.TEXT[0])
dialogue.show_text(self.TEXT[2])
self.text_index = 0
# temporary dialogue skip
dialogue.set_name("Tony")
self.slime_bag.hide()
self.halt(self.move_board)
self.take_board()
platform = self.get_game().platform
platform.activate()
platform.set_glowing(platform.get_buttons_from_edges(
[self.TUTORIAL_MOVES[self.tutorial_index]]))
self.get_game().chemtrails.activate()
self.text_index = 2
def give_board(self):
self.skateboard.location.center = self.SKATEBOARD_START
self.skateboard_step = get_step(self.skateboard.location.center, self.slime_bag.location.center, 2)
self.skateboard.unhide()
self.play(self.move_board)
def move_board(self):
self.skateboard.move(*self.skateboard_step)
if self.skateboard.location.colliderect(self.slime_bag.location.inflate(-30, -30)):
self.halt(self.move_board)
self.play(self.take_board, delay=2000, play_once=True)
self.get_audio().play_sfx("go")
def take_board(self):
self.skateboard.hide()
self.slime_bag.set_frameset("board")
def activate_boss(self):
self.deactivate()
self.get_game().boss.start_level(0)
def start_wipe(self):
self.get_game().wipe.start(self.activate_boss)
def update(self):
if self.active:
Animation.update(self)
# dialogue = self.get_game().dialogue
wipe = self.get_game().wipe
if not wipe.is_playing() and not self.is_playing(self.start) and not self.text_index == 2:
if self.advance_prompt.check_first_press():
self.advance_prompt.press_first()
elif self.advance_prompt.check_second_press():
if dialogue.is_playing():
dialogue.show_all()
else:
if self.text_index < len(self.TEXT) - 1:
self.text_index += 1
if self.text_index == 1:
dialogue.set_name("Tony")
self.give_board()
elif self.text_index == 2:
self.slime_bag.hide()
self.halt(self.move_board)
self.take_board()
platform = self.get_game().platform
platform.activate()
platform.set_glowing(platform.get_buttons_from_edges(
[self.TUTORIAL_MOVES[self.tutorial_index]]))
self.get_game().chemtrails.activate()
dialogue.show_text(self.TEXT[self.text_index])
else:
self.start_wipe()
# self.get_game().platform.unpress()
self.advance_prompt.cancel_first_press()
elif not wipe.is_playing() and self.text_index == 2:
platform = self.get_game().platform
if platform.get_edge_pressed() == self.TUTORIAL_MOVES[self.tutorial_index]:
self.tutorial_index += 1
self.get_audio().play_sfx("land_0")
if self.tutorial_index == len(self.TUTORIAL_MOVES):
# self.text_index += 1
# self.advance_prompt.cancel_first_press()
platform.set_glowing([])
self.start_wipe()
# dialogue.show_text(self.TEXT[self.text_index])
else:
platform.set_glowing(platform.get_buttons_from_edges(
[self.TUTORIAL_MOVES[self.tutorial_index]]))
self.get_game().tony.update()
self.slime_bag.update()
self.skateboard.update()
for word in self.words:
word.update()
self.get_game().platform.update()
# self.get_game().dialogue.update()
# if not wipe.is_playing() and not self.is_playing(self.start) and \
# not self.text_index == 2:
# self.advance_prompt.update()
if not wipe.is_playing() and not self.text_index == 2:
self.skip_prompt.update()
class SkipPrompt(GameChild):
def __init__(self, parent, callback):
@ -1469,9 +1343,17 @@ class Boss(Animation):
def __init__(self, parent):
Animation.__init__(self, parent)
self.kool_man = RainbowSprite(self, load(self.get_resource("Kool_man_waah.png")).convert_alpha(), 30)
self.visitor = RainbowSprite(self, load(self.get_resource("Visitor.png")).convert_alpha(), 30)
self.spoopy = RainbowSprite(self, load(self.get_resource("Spoopy.png")).convert_alpha(), 30)
if self.get_configuration("display", "effects"):
self.kool_man = RainbowSprite(self, load(self.get_resource("Kool_man_waah.png")).convert_alpha(), hue_shift)
self.visitor = RainbowSprite(self, load(self.get_resource("Visitor.png")).convert_alpha(), hue_shift)
self.spoopy = RainbowSprite(self, load(self.get_resource("Spoopy.png")).convert_alpha(), hue_shift)
else:
self.kool_man = Sprite(self)
self.kool_man.load_from_path("Kool_man_waah.png", True)
self.visitor = Sprite(self)
self.visitor.load_from_path("Visitor.png", True)
self.spoopy = Sprite(self)
self.spoopy.load_from_path("Spoopy.png", True)
for sprite in self.kool_man, self.visitor, self.spoopy:
sprite.location.topleft = 100, 0
self.health = Health(self)
@ -1925,7 +1807,10 @@ class Sword(Animation):
swords = self.swords = []
for root in "Sword_kool_man/", "Sword_visitor/", "Sword_spoopy/":
swords.append([[], [], [], [], [], []])
for path in sorted(iglob(join(self.get_resource(root), "*.png"))):
base_image_paths = sorted(iglob(join(self.get_resource(root), "*.png")))
if not self.get_configuration("display", "effects"):
base_image_paths = [base_image_paths[0]]
for path in base_image_paths:
base = load(self.get_resource(path)).convert_alpha()
for position in range(6):
if position == NS.N or position == NS.S:

79
README
View File

@ -1,5 +1,78 @@
Requires pyserial (https://pypi.org/project/pyserial/)
Scrapeboard is an arcade game in development by Frank DeMarco (@diskmem) and Blake Andrews (@snakesandrews)
If you have Python Package Installer, you can run
It requires a custom pad and board to play. To learn more about the project visit https://scrape.nugget.fun
pip install pyserial
This repository can be used to run either the full arcade version or the keyboard-only mode for testing
################
# REQUIREMENTS #
################
The game requires Python and Pygame. The Python version used for development is Python 3.9. The Pygame version
is 1.9.6.
To install python with pip:
pip install pygame
Once Python and Pygame are installed, you should be able to run either:
./OPEN-GAME
or
./OPEN-GAME --no-serial
to start the game in either full arcade mode or keyboard only mode. See below for more about serial input and
keyboard input modes.
##########
# SERIAL #
##########
To run the game using the custom skateboard and dance pads, the Arduino attached to the pads must be plugged
into USB, and the pyserial package must be installed on this computer (https://pypi.org/project/pyserial/)
If you have Python Package Installer, you can run this to install pyserial:
pip install pyserial
The Arduino must be loaded with the program at serial/serial2/serial2.ino and connected to USB. The game
will try to detect the Arduino, but to specify a specific port you can use the config file or command
line.
If you don't have the board, pad and Arduino, you can test the game using keyboard-only mode.
########
# KEYS #
########
For testing, there is keyboard input. To run in keyboard only mode use:
./OPEN-GAME --no-serial
The O, P, L, and ; keys simulate the dance pads and your fingers simulate the board
___ ___
| 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| ;|
|___|___| or you can use arrow keys
UP = top left pad, RIGHT = top right pad, DOWN = bottom left pad, LEFT = bottom right pad
Other keys:
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).
###########
# OPTIONS #
###########
The full list of configurable values is in the file called `config`. There are also command line flags that
can override config values:
./OPEN-GAME -h
The --minimize-load-time flag can be useful when testing because it sacrifices some effects to load the game
quickly.

27
config
View File

@ -1,7 +1,7 @@
[setup]
license = Public Domain
title = Scrapeboard
url = http://shampoo.ooo/games/esb
url = https://scrape.nugget.fun
version = 0.2.3
init-script = OPEN-GAME
additional-packages = lib
@ -9,13 +9,18 @@ data-exclude = local/, *.pyc, .git*, README, build/, dist/, *.egg-info, *.py, MA
[display]
caption = Scrapeboard
show-framerate = False
show-framerate = no
dimensions = 640, 480
fullscreen = False
fullscreen = no
attract-gif-alpha = 0.95
effects = yes
[system]
# will force set display->effects to off
minimize-load-time = yes
[mouse]
visible = False
visible = no
[keys]
quit = K_ESCAPE
@ -23,12 +28,12 @@ up = K_u
[audio]
sfx-volume = 0.8
panel-enabled = True
panel-enabled = yes
volume = 1.0
[input]
buffer = 0
arduino-port = /dev/ttyACM1
arduino-port = /dev/ttyACM0
serial = True
[time]
@ -40,11 +45,11 @@ attract-gif-length = 10000
attract-board-length = 3600
[bgm]
title = resource/bgm/title.ogg, 1.00
level_0 = /home/frank/storage/audio/bgm/bat-tree-habitat-key/level-0.wav, 1.00
level_1 = /home/frank/storage/audio/bgm/esp-hadouken/Cube-Levers.ogg, 1.00
level_2 = /home/frank/storage/audio/bgm/esp-hadouken/Bog.ogg, 1.00
end = /home/frank/storage/audio/bgm/phone-call-from-dark-magnet.wav, 1.00
title = resource/bgm/title.ogg, .65
level_0 = /home/frank/storage/audio/bgm/bat-tree-habitat-key/level-0.wav, .65
level_1 = /home/frank/storage/audio/bgm/esp-hadouken/Cube-Levers.ogg, .65
level_2 = /home/frank/storage/audio/bgm/esp-hadouken/Bog.ogg, .65
end = /home/frank/storage/audio/bgm/phone-call-from-magnet.wav, .65
[pads]
nw_color = #00FF88