USER
dari script berikut ini tambahkan fitur heat level dan ini bisa di bool pada bagian atas script , yg dimana square jika collision nya terjadi terlalu cepat maka glow intensity nya akan bertambah, jadi ini di hitung dari jarak jarak collision, dan pertambahan intensity harus lah smooth, jadi jika collision yg terjadi hanya lumayan maka glow nya bertambah lumayan juga, jika sangat cepat maka glow intensity nya juga signifikan, buatkan agar level heat nya ini smooth dan memanfaatkan level level dari jarak collision yg terjadi, buatkan dengan benar dan tepat tanpa mempengaruhi fitur fitur yg sudah ada, sesuaikan dengan fitur yg sudah ada, buatkan dengan tepat dan lengkap : import pygame
import mido
import sys
import random
import requests
import colorsys
import os
import math
import cv2
import numpy as np
from os.path import join, isfile
from time import time as get_current_time
from typing import Union, Optional, Any
from enum import Enum
from math import sin, pi
pygame.mixer.init()
pygame.init()
FRAMERATE = 60
# =========================
# Customizable Configuration
# =========================
# Midi Selection
SONG = "rush-e.mid"
# Screen Resolution
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
# Other Customizable Settings
SQUARE_SPEED = 600
SQUARE_SIZE = 40
BOUNCE_MIN_SPACING = 50
DIRECTION_CHANGE = 30
EMPTY_SQUARE = True
# Particle Settings
PARTICLE_BOUNCE = False
PARTICLE_TRAIL = False
TRAIL_COLOR_BASED_ON_SQUARE = False
PARTICLE_SPEED = 5
# Peg Settings
BOUNCE_PEG = True
PEG_COLOR = (255, 255, 255)
PEG_REMOVE = True
PEG_MULTI_COLOR = False
NO_COLOR = False
# Glow Settings
SQUARE_GLOW = True
PEG_GLOW = True
GLOW_RANDOM_COLOR = False
# Star Settings
STAR = True
STAR_COLOR = (255, 255, 255)
STAR_DENSITY = 0.0001
# =========================
def color_hunt():
def is_bright(hex_color):
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
luminance = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]
r, g, b = [x / 255.0 for x in rgb]
h, s, v = colorsys.rgb_to_hsv(r, g, b)
return luminance >= 130 and s >= 0.3
while True:
url = 'https://colorhunt.co/php/feed.php'
headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
data = {
'step': 0,
'sort': 'random',
'tags': ''
}
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
palettes = response.json()
color_pairs = []
for palette in palettes:
code = palette['code']
hex_colors = [f'#{code[i:i+6].upper()}' for i in range(0, len(code), 6)]
if len(hex_colors) == 4 and all(is_bright(hex_color) for hex_color in hex_colors):
color_pairs.append(tuple(hex_colors))
if color_pairs:
selected_color_pair = random.choice(color_pairs)
return selected_color_pair
else:
raise Exception(f"Failed to retrieve data. Status code: {response.status_code}")
selected_colors = color_hunt()
color1, color2, color3, color4 = selected_colors
class Config:
# Constants
SQUARE_SIZE = SQUARE_SIZE
PARTICLE_SPEED = PARTICLE_SPEED
CAMERA_SPEED = 500
# Colors
color_themes = {
"dark": {
"hallway": pygame.Color('#2D033B'),
"background": pygame.Color('#070F2B'),
"square": [
pygame.Color(color1),
pygame.Color(color2),
pygame.Color(color3),
pygame.Color(color4)
],
"peg": pygame.Color(PEG_COLOR), # Added peg color
}
}
# Configuration
script_dir = os.path.dirname(os.path.abspath(__file__))
assets = os.path.join(script_dir, 'assets/icon.png')
song = os.path.join(script_dir, 'songs')
font = os.path.join(script_dir, 'assets/poppins-regular.ttf')
SCREEN_WIDTH = SCREEN_WIDTH
SCREEN_HEIGHT = SCREEN_HEIGHT
theme: Optional[str] = "dark"
seed: Optional[int] = None
camera_mode: Optional[int] = 2
start_playing_delay = 5000
max_notes: Optional[int] = None
bounce_min_spacing: Optional[float] = BOUNCE_MIN_SPACING
square_speed: Optional[int] = SQUARE_SPEED
volume: Optional[int] = 100
music_offset: Optional[int] = 0
direction_change_chance: Optional[int] = DIRECTION_CHANGE
particle_trail = PARTICLE_TRAIL
do_color_bounce_pegs = BOUNCE_PEG
do_particles_on_bounce = PARTICLE_BOUNCE
# Non-configurable settings
backtrack_chance: Optional[float] = 0.02
backtrack_amount: Optional[int] = 40
square_swipe_anim_speed: Optional[int] = 4
particle_amount = 20
# Glow effect settings for square
square_glow = SQUARE_GLOW
square_glow_duration = 0.2
glow_intensity = 15 # Adjusted intensity for better visibility
square_min_glow = 0 # Lower value for slight glow when no collision
# Glow effect settings for pegs
peg_glow = PEG_GLOW
peg_glow_duration = 0.5
peg_glow_intensity = 15 # Adjusted intensity for better visibility
peg_min_glow = 7
if GLOW_RANDOM_COLOR:
peg_glow_color = pygame.Color(random.choice(selected_colors))
square_glow_color = pygame.Color(random.choice(selected_colors))
else:
peg_glow_color = pygame.Color(255, 255, 255)
square_glow_color = pygame.Color(255, 255, 255)
# Star settings
star_color = STAR_COLOR
star_density = STAR_DENSITY
# Other random stuff
current_song = None
dt = 0.01
def get_colors():
return Config.color_themes.get(Config.theme, Config.color_themes["dark"])
def read_midi_file(file):
midi_file = mido.MidiFile(file=file)
notes = []
current_time = 0
for msg in midi_file:
if msg.type == 'note_on' and msg.velocity != 0:
timestamp = current_time + msg.time
notes.append(round(timestamp*1000)/1000)
current_time += msg.time
return notes
def remove_too_close_values(lst: list[float], threshold=30) -> list[float]:
"""Assumes the list is sorted"""
new = []
before = None
for _ in lst:
if before is None:
before = _
new.append(_)
continue
if before+threshold/1000 > _:
continue
before = _
new.append(_)
return new
def fix_overlap(rects: list[pygame.Rect], callback=None):
"""
Optimized function to merge overlapping rectangles efficiently.
"""
if callback is None:
callback = lambda _: None
rects = rects.copy()
# Sort rectangles based on x coordinate
rects.sort(key=lambda r: (r.x, r.y))
merged_rects = []
for rect in rects:
if not merged_rects:
merged_rects.append(rect)
else:
last = merged_rects[-1]
if last.colliderect(rect):
merged_rect = last.union(rect)
merged_rects[-1] = merged_rect
else:
merged_rects.append(rect)
callback("Finished merging rectangles")
return merged_rects
def get_font(size: int = 24) -> pygame.font.Font:
font_path = Config.font
return pygame.font.Font(font_path, size)
class MapLoadingFailureError(Exception):
"""The map fails to load (recurs function fails)"""
pass
class UserCancelsLoadingError(Exception):
"""User cancels the loading screen"""
pass
def interpolate_fn(n):
"""Interpolate sigmoidally from 0-1"""
n = min(max(n, 0), 1)
return sin(pi * (n - 0.5)) / 2 + 0.5
class Particle:
SPEED_VARIATION = 4
SIZE_MIN = 7
SIZE_MAX = 14
AGE_RATE = 20
SLOW_DOWN_RATE = 1.2
def __init__(self, pos: list[float], delta: list[float], invert_color: bool = False, color: Optional[tuple] = None):
self.pos = pos.copy()
self.size = random.randint(Particle.SIZE_MIN, Particle.SIZE_MAX)
self.delta = delta.copy()
self.delta[0] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
self.delta[1] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
if color is not None:
self.color = color
else:
# Default color if color is not given
self.color = get_colors()["hallway"] if not invert_color else get_colors()["background"]
# Adjust color to white for particles
self.color = (255, 255, 255)
def age(self):
self.size -= Particle.AGE_RATE*Config.dt
self.x += self.delta[0] * Config.PARTICLE_SPEED
self.y += self.delta[1] * Config.PARTICLE_SPEED
if Config.dt != 0:
self.delta[0] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
self.delta[1] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
return self.size <= 0
@property
def x(self):
return self.pos[0]
@x.setter
def x(self, val: float):
self.pos[0] = val
@property
def y(self):
return self.pos[1]
@y.setter
def y(self, val: float):
self.pos[1] = val
@property
def rect(self):
return pygame.Rect(self.x-self.size/2, self.y-self.size/2, *(2*[self.size]))
class Bounce:
def __init__(self, sq_pos: list[float], sq_dir: list[int], time: float, bounce_dir: int):
self.square_pos = sq_pos # New square position
self.square_dir = sq_dir # New square direction
self.bounce_dir = bounce_dir # Bounce direction for squish effect; 0 or 1
self.time = time # Time during bounce
def get_collision_rect(self):
sx, sy = self.square_pos
if self.bounce_dir == 0:
# Bounce left or right wall
if self.square_dir[0] == -1:
# Right wall
return pygame.Rect(
sx+Config.SQUARE_SIZE/2+1,
sy-10,
10,
20
)
elif self.square_dir[0] == 1:
# Left wall
return pygame.Rect(
sx-10-Config.SQUARE_SIZE/2-1,
sy-10,
10,
20
)
elif self.bounce_dir == 1:
# Bounce top or bottom wall
if self.square_dir[1] == -1:
# Bottom wall
return pygame.Rect(
sx-10,
sy+Config.SQUARE_SIZE/2+1,
20,
10
)
elif self.square_dir[1] == 1:
# Top wall
return pygame.Rect(
sx-10,
sy-10-Config.SQUARE_SIZE/2-1,
20,
10
)
def copy(self) -> "Bounce":
return Bounce(self.square_pos.copy(), self.square_dir.copy(), self.time, self.bounce_dir)
def __repr__(self):
return f"<Bounce(sq_pos={self.square_pos}, sq_dir={self.square_dir}, time={self.time}, dir={self.bounce_dir})"
# Glow effect functions
def create_border(image: np.ndarray, margin: int, thickness: int, color: tuple) -> np.ndarray:
height, width = image.shape[:2]
cv2.rectangle(image, (margin, margin), (width - margin, height - margin), color, thickness=thickness)
return image
def apply_blooming(image: np.ndarray) -> np.ndarray:
# Provide some blurring to image, to create some bloom.
cv2.GaussianBlur(image, ksize=(15, 15), sigmaX=20, sigmaY=20, dst=image)
cv2.blur(image, ksize=(12, 12), dst=image)
return image
def glowing_border(image: np.ndarray, margin=20, thickness=20, color=(255, 255, 255)):
"""
Create a glowing border around an image.
"""
# Generate the colored border.
image = create_border(image, margin, thickness, color)
# Apply the bloom effect to the image with the border.
image = apply_blooming(image)
# Reassert the original border to enhance the edges.
image = create_border(image, margin - 1, 1, color)
image = create_border(image, margin + 1, 1, color)
return image
def make_glowy2(size, color, intensity=0) -> pygame.Surface:
image = np.zeros((*size[::-1], 3), dtype=np.uint8)
border_color_rgb = color # Assuming color is RGB tuple
border = glowing_border(image.copy(), color=border_color_rgb, thickness=intensity)
return pygame.surfarray.make_surface(np.fliplr(np.rot90(border, k=-1)))
class Square:
glowing_surface_cache = {}
def __init__(self, x: float = 0, y: float = 0, dx: int = 1, dy: int = 1):
self.pos: list[float] = [x, y]
self.dir: list[int] = [dx, dy]
self.last_bounce_time = -100
self.latest_bounce_direction = 0 # 0 = horiz, 1 = vert
self.past_colors = []
self.died = False
self.time_since_glow_start = -1000 # For glow effect
def register_past_color(self, col: tuple[int, int, int]):
for _ in range(max(Config.square_swipe_anim_speed, 1)):
self.past_colors.insert(0, col)
while len(self.past_colors) > Config.SQUARE_SIZE * 4 / 5:
self.past_colors.pop()
def get_surface(self, size: tuple[int, int]):
ss = int(Config.SQUARE_SIZE * 4 / 5)
surf = pygame.Surface((ss, ss))
for index, col in enumerate(self.past_colors):
y = index if self.dir_y != 1 else ss - 1 - index
pygame.draw.line(surf, col, (0, y), (ss, y))
return pygame.transform.scale(surf, size)
def copy(self) -> "Square":
new = Square(*self.pos, *self.dir)
new.last_bounce_time = self.last_bounce_time
new.latest_bounce_direction = self.latest_bounce_direction
return new
def get_current_color(self):
if EMPTY_SQUARE:
return (255, 255, 255) # Border color is white
else:
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
return get_colors()["square"][square_color_index % len(get_colors()["square"])]
def compute_glowy_surface(self, rect, val):
cache_key = (rect.size, val)
if cache_key in Square.glowing_surface_cache:
return Square.glowing_surface_cache[cache_key]
glowy_borders = make_glowy2((rect.width + 40, rect.height + 40), Config.square_glow_color, val)
surface = pygame.Surface(rect.inflate(100, 100).size, pygame.SRCALPHA)
surface.blit(glowy_borders, (20, 20), special_flags=pygame.BLEND_RGBA_ADD)
Square.glowing_surface_cache[cache_key] = surface
return surface
def draw_glowing3(self, win, rect):
if self.died:
return
if Config.square_glow:
current_ticks = pygame.time.get_ticks()
time_since_glow = current_ticks - self.time_since_glow_start
if EMPTY_SQUARE:
# Glow is always slightly on, increases during collision
if time_since_glow < Config.square_glow_duration * 1000:
progress = 1 - (time_since_glow) / (Config.square_glow_duration * 1000)
val = int(progress * Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
# Minimum glow intensity when not colliding
val = Config.square_min_glow
else:
# Glow only during collision
if time_since_glow < Config.square_glow_duration * 1000:
progress = 1 - (time_since_glow) / (Config.square_glow_duration * 1000)
val = int(progress * Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
return # Do not draw glow if not within glow duration
if val > 0:
surf = self.compute_glowy_surface(rect, val)
win.blit(surf, rect.move(-40, -40).topleft, special_flags=pygame.BLEND_RGBA_ADD)
@property
def x(self):
return self.pos[0]
@property
def y(self):
return self.pos[1]
def draw(self, screen: pygame.Surface, sqrect: pygame.Rect):
if self.died:
return
# Draw glowing square if glowing
self.draw_glowing3(screen, sqrect)
if EMPTY_SQUARE:
# Draw the square as an empty square with white border
border_color = (255, 255, 255)
border_thickness = 2 # Adjust as needed
pygame.draw.rect(screen, border_color, sqrect, border_thickness)
else:
# Default drawing of the square
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
self.register_past_color(get_colors()["square"][square_color_index % len(get_colors()["square"])])
# Draw the square itself
pygame.draw.rect(screen, (0, 0, 0), sqrect)
sq_surf = self.get_surface(
tuple(sqrect.inflate(-int(Config.SQUARE_SIZE / 5), -int(Config.SQUARE_SIZE / 5))[2:]))
screen.blit(sq_surf, sq_surf.get_rect(center=sqrect.center))
@x.setter
def x(self, val: int):
self.pos[0] = val
@y.setter
def y(self, val: int):
self.pos[1] = val
@property
def dir_x(self):
return self.dir[0]
@property
def dir_y(self):
return self.dir[1]
@property
def rect(self):
return pygame.Rect(self.x - Config.SQUARE_SIZE / 2, self.y - Config.SQUARE_SIZE / 2,
*([Config.SQUARE_SIZE] * 2))
def obey_bounce(self, bounce: Bounce):
# Planned bounces
self.pos = bounce.square_pos.copy()
self.dir = bounce.square_dir.copy()
self.latest_bounce_direction = bounce.bounce_dir
self.last_bounce_time = bounce.time
# Start glow effect
self.time_since_glow_start = pygame.time.get_ticks()
return
def reg_move(self, use_dt: bool = True):
self.x += self.dir_x * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
self.y += self.dir_y * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
class Camera:
def __init__(self, x: int = 0, y: int = 0):
self.x = x
self.y = y
self.locked_on_square = True
self.lock_type: CameraFollow = CameraFollow(Config.camera_mode)
def attempt_movement(self):
if not self.locked_on_square:
keys = pygame.key.get_pressed()
shift_modifier = (keys[pygame.K_LSHIFT] | keys[pygame.K_RSHIFT]) + 1
self.x += (keys[pygame.K_d] - keys[pygame.K_a]) * Config.CAMERA_SPEED * shift_modifier / FRAMERATE
self.y += (keys[pygame.K_s] - keys[pygame.K_w]) * Config.CAMERA_SPEED * shift_modifier / FRAMERATE
@property
def pos(self):
return self.x, self.y
@pos.setter
def pos(self, val: Union[tuple[int, int], list[int]]):
self.x, self.y = val
def offset(self, pos_or_rect: Union[pygame.Rect, tuple[int, int]]) -> Union[pygame.Rect, list[int]]:
if isinstance(pos_or_rect, pygame.Rect):
return pos_or_rect.move(-self.x, -self.y)
else:
return [pos_or_rect[0]-self.x, pos_or_rect[1]-self.y]
def apply_parallax(self, pos: tuple[float, float], depth: float) -> list[float]:
# Parallax factor: nearer stars move faster than distant ones
parallax_factor = 0.2 + depth * 0.8 # From 0.2 to 1.0
screen_x = (pos[0] - self.x) * parallax_factor
screen_y = (pos[1] - self.y) * parallax_factor
return [screen_x, screen_y]
def follow(self, square: Square):
# Square in center
if self.lock_type == CameraFollow.Center:
self.pos = [square.x - Config.SCREEN_WIDTH / 2, square.y - Config.SCREEN_HEIGHT / 2]
# Smooth camera
if self.lock_type == CameraFollow.Smoothed:
easing_rate = 3
self.x = (square.x - Config.SCREEN_WIDTH / 2) * easing_rate * Config.dt + self.x - easing_rate * self.x * Config.dt
self.y = (square.y - Config.SCREEN_HEIGHT / 2) * easing_rate * Config.dt + self.y - easing_rate * self.y * Config.dt
class CameraFollow(Enum):
Center = 0 # Center the square
Smoothed = 2 # Smoothed camera
class Star:
def __init__(self, pos: tuple[float, float], depth: float):
self.initial_pos = list(pos)
self.depth = depth # between 0 (far) and 1 (near)
size_min = 0.5
size_max = 2
self.size = size_min + (1 - self.depth) * (size_max - size_min)
self.phase = random.uniform(0, 2 * math.pi) # For random phase
self.brightness = 0 # Will be updated in the update method
self.color = Config.star_color # Use star color from Config
def update(self, time):
# Adjust brightness over time to create blinking effect
blink_speed = 10 # can adjust to get desired speed
brightness_variation = (math.sin(blink_speed * time + self.phase) + 1) / 2 # Normalize between 0 and 1
min_brightness = 100
max_brightness = 255
brightness = min_brightness + brightness_variation * (max_brightness - min_brightness)
self.color = tuple(min(255, int(c * (brightness / 255))) for c in Config.star_color) # Adjust color based on brightness
def get_screen_pos(self, camera: Camera):
parallax_factor = 0.2 + self.depth * 0.8 # from 0.2 to 1.0
screen_x = (self.initial_pos[0] - camera.x) * parallax_factor
screen_y = (self.initial_pos[1] - camera.y) * parallax_factor
return [screen_x, screen_y]
class Peg:
glowing_surface_cache = {}
def __init__(self, rect: pygame.Rect, color: tuple, collision_time: float):
self.rect = rect
self.color = color
self.collision_time = collision_time # The time when the square is supposed to bounce on this peg
self.time_since_glow_start = -1000 # For glow effect
self.visible = False # Visibility flag
def update_glow(self, current_time):
# Update the glow start time
self.time_since_glow_start = current_time
self.visible = True # Set visibility to True after collision
def compute_glowy_surface(self, rect, val):
cache_key = (rect.size, val)
if cache_key in Peg.glowing_surface_cache:
return Peg.glowing_surface_cache[cache_key]
glowy_borders = make_glowy2((rect.width + 40, rect.height + 40), Config.peg_glow_color, val)
surface = pygame.Surface(rect.inflate(100, 100).size, pygame.SRCALPHA)
surface.blit(glowy_borders, (20, 20), special_flags=pygame.BLEND_RGBA_ADD)
Peg.glowing_surface_cache[cache_key] = surface
return surface
def draw_glow(self, screen, rect):
if Config.peg_glow:
current_ticks = pygame.time.get_ticks()
elapsed_time = (current_ticks - self.time_since_glow_start) / 1000.0
if elapsed_time < Config.peg_glow_duration:
progress = 1 - (elapsed_time) / Config.peg_glow_duration
val = int(progress * Config.peg_glow_intensity)
val = max(val, Config.peg_min_glow)
surf = self.compute_glowy_surface(rect, val)
screen.blit(surf, rect.move(-40, -40).topleft, special_flags=pygame.BLEND_RGBA_ADD)
def draw(self, screen: pygame.Surface, camera: Camera, current_time: float):
offsetted = camera.offset(self.rect)
if screen.get_rect().colliderect(offsetted):
if self.visible:
# After collision time, draw the peg and glow effect
self.draw_glow(screen, offsetted)
pygame.draw.rect(screen, self.color, offsetted)
else:
color = (get_colors()["background"])
pygame.draw.rect(screen, color, offsetted)
pass # Peg remains invisible before collision
class World:
"""It's a cruel world out there"""
def __init__(self):
self.future_bounces: list[Bounce] = []
self.past_bounces: list[Bounce] = []
self.start_time = 0
self.time = 0
self.pegs: list[Peg] = [] # List of Peg objects
self.particles: list[Particle] = []
self.stars: list[Star] = []
self.timestamps = []
self.square = Square()
self.safe_areas: list[pygame.Rect] = []
def update_time(self) -> None:
self.time = get_current_time() - self.start_time
def get_next_bounce(self) -> Bounce:
"""Also pops the bounce from the future_bounces list"""
bounce = self.future_bounces.pop(0)
self.past_bounces.append(bounce)
return bounce
def add_bounce_particles(self, sp: list[float], sd: list[float], color: tuple):
for _ in range(Config.particle_amount):
new = Particle([sp[0] + random.randint(-10, 10), sp[1] + random.randint(-10, 10)], sd, color=color)
self.particles.append(new)
def handle_bouncing(self, square: Square):
if len(self.future_bounces):
if (self.time * 1000 + Config.music_offset)/1000 > self.future_bounces[0].time:
current_bounce = self.get_next_bounce()
before = square.dir.copy()
square.obey_bounce(current_bounce)
changed = square.dir.copy()
for _ in range(2):
if before[_] == changed[_]:
changed[_] = 0
else:
changed[_] = -changed[_]
if Config.do_particles_on_bounce:
square_color = square.get_current_color()
self.add_bounce_particles(square.pos, changed, color=square_color)
# Stop square at end
if len(self.future_bounces) == 0:
square.dir = [0, 0]
square.pos = current_bounce.square_pos
# Update the corresponding peg's glow
index = len(self.past_bounces) - 1 # Index of the current bounce
if index < len(self.pegs):
peg = self.pegs[index]
peg.update_glow(pygame.time.get_ticks())
def generate_stars(self):
self.stars = []
# Calculate total safe area
total_safe_area = sum(area.width * area.height for area in self.safe_areas)
star_density = Config.star_density # Use star density from Config
total_num_stars = int(total_safe_area * star_density)
min_distance_squared = (20) ** 2 # Avoid clustering
# Get bounding rectangle of all safe areas
min_x = min(area.left for area in self.safe_areas)
max_x = max(area.right for area in self.safe_areas)
min_y = min(area.top for area in self.safe_areas)
max_y = max(area.bottom for area in self.safe_areas)
bounding_rect = pygame.Rect(min_x, min_y, max_x - min_x, max_y - min_y)
stars = []
attempts = 0
max_attempts = total_num_stars * 10
while len(stars) < total_num_stars and attempts < max_attempts:
x = random.uniform(bounding_rect.left, bounding_rect.right)
y = random.uniform(bounding_rect.top, bounding_rect.bottom)
point = (x, y)
# Ensure point is within safe area
inside_safe_area = any(area.collidepoint(point) for area in self.safe_areas)
if inside_safe_area:
# Ensure minimum distance between stars
if all((star.initial_pos[0] - x) ** 2 + (star.initial_pos[1] - y) ** 2 >= min_distance_squared for star in stars):
depth = random.uniform(0, 1)
star = Star((x, y), depth)
stars.append(star)
attempts += 1
self.stars = stars
def gen_future_bounces(self, _start_notes: list[float], percent_update_callback):
"""Recursive function with optimized overlap checking"""
total_notes = len(_start_notes)
max_percent = 0
path = []
safe_areas = []
force_return = 0
def recurs(
square: Square,
notes: list[float],
bounces_so_far: list[Bounce] = None,
prev_index_priority=None,
t: float = 0,
depth: int = 0
) -> Union[list[Bounce], bool]:
nonlocal force_return, max_percent
if prev_index_priority is None:
prev_index_priority = [0, 1]
if bounces_so_far is None:
bounces_so_far = []
if total_notes:
gone_through_percent = (total_notes-len(notes)) * 100 // total_notes
while gone_through_percent > max_percent:
max_percent += 1
if percent_update_callback(f"{max_percent}% done generating map"):
raise UserCancelsLoadingError()
all_bounce_rects = [_bounc.get_collision_rect() for _bounc in bounces_so_far]
if len(notes) == 0:
return bounces_so_far
path_segment_start = len(path)
start_rect = square.rect.copy()
while True:
t += 1/FRAMERATE
square.reg_move(False)
path.append(square.rect)
if t > notes[0]:
# No collision (we good)
bounce_indexes = prev_index_priority
# Randomly change direction every X% of the time
if random.random() * 100 < Config.direction_change_chance:
bounce_indexes = list(bounce_indexes.__reversed__())
# Add safe area
safe_areas.append(start_rect.union(square.rect))
for direction_to_bounce in bounce_indexes:
square.dir[direction_to_bounce] *= -1
bounces_so_far.append(Bounce(square.pos.copy(), square.dir.copy(), t, direction_to_bounce))
toextend = recurs(
square=square.copy(),
notes=notes[1:],
bounces_so_far=[_b.copy() for _b in bounces_so_far],
t=t,
prev_index_priority=bounce_indexes.copy(),
depth=depth+1
)
if toextend:
return toextend
else:
bounces_so_far = bounces_so_far[:-1]
square.dir[direction_to_bounce] *= -1
# Backtrack if necessary
if force_return:
force_return -= 1
while len(path) != path_segment_start:
path.pop()
return False
continue
while len(path) != path_segment_start:
path.pop()
return False
othercheck = False
if len(bounces_so_far):
othercheck = bounces_so_far[-1].get_collision_rect().collidelist(path[:-10])+1
if square.rect.collidelist(all_bounce_rects) != -1 or othercheck:
if depth > 200:
if random.random() < Config.backtrack_chance:
max_percent -= (Config.backtrack_amount * 100 // total_notes) + 1
force_return = Config.backtrack_amount
while len(path) != path_segment_start:
path.pop()
return False
_start_notes = _start_notes[:Config.max_notes] if Config.max_notes is not None else _start_notes
self.future_bounces = recurs(
square=self.square.copy(),
notes=remove_too_close_values(
[_sn for _sn in _start_notes],
threshold=Config.bounce_min_spacing
)
)
if self.future_bounces is False:
raise MapLoadingFailureError("The map failed to generate because of the recursion function. " +
"If the MIDI has too many notes too close, it may not generate. " +
"Maybe try changing the \"bounce min spacing\", \"square speed\", or \"change dir chance\" in the config.")
if len(self.future_bounces) == 0:
raise MapLoadingFailureError("Map safe area list is empty. Please report this issue.")
percent_update_callback("Merging overlapping safe areas")
# Merging overlapping safe areas more efficiently
self.safe_areas = fix_overlap(safe_areas, percent_update_callback)
# Generate colors for pegs
def gather_colors(num_colors=20):
colors = []
while len(colors) < num_colors:
new_colors = color_hunt()
rgb_colors = [tuple(int(hex_color[i:i+2], 16) for i in (1, 3, 5)) for hex_color in new_colors]
colors.extend(rgb_colors)
return colors[:num_colors]
collected_colors = gather_colors()
# Setting random colors for the generated pegs
colors = [random.choice(collected_colors) for _ in self.future_bounces]
if not PEG_REMOVE:
if PEG_MULTI_COLOR:
collected_colors = gather_colors()
# Setting random colors for the generated pegs
colors = [random.choice(collected_colors) for _ in self.future_bounces]
elif NO_COLOR:
colors = [get_colors()["background"] for _ in self.future_bounces]
else:
# Use the peg color from get_colors() if peg_multi_color is False
colors = [get_colors()["peg"] for _ in self.future_bounces]
else:
colors = [get_colors()["hallway"] for _ in self.future_bounces]
# Create pegs
self.pegs = []
for i in range(len(self.future_bounces)):
bounce_rect = self.future_bounces[i].get_collision_rect()
color = colors[i]
collision_time = self.future_bounces[i].time
peg = Peg(bounce_rect, color, collision_time)
self.pegs.append(peg)
# Generate stars in safe areas
if STAR:
self.generate_stars()
return self.safe_areas
class Game:
def __init__(self):
self.active = False
self.notes = []
self.camera = Camera()
self.world = World()
self.safe_areas: list[pygame.Rect] = []
self.music_has_played = False
self.offset_happened = False
def start_song(self, screen: pygame.Surface):
random.seed(Config.seed)
# Load song and notes
with open(Config.current_song.fp, 'rb') as f:
notes = read_midi_file(file=f)
notes = sorted([note for note in notes])
self.notes = notes
# Other settings
self.world = World()
self.music_has_played = False
self.offset_happened = False
self.camera.lock_type = CameraFollow(Config.camera_mode)
screen.fill(get_colors()["background"])
pygame.display.flip()
def update_loading_screen(message: str):
screen.fill(get_colors()["background"], pygame.Rect(0, 0, Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT))
font_size = int(Config.SCREEN_HEIGHT * 0.05)
loading_text = get_font(font_size).render(message, True, get_colors()["hallway"])
text_rect = loading_text.get_rect()
text_rect.center = (Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2)
screen.blit(loading_text, text_rect)
pygame.display.flip()
try:
self.safe_areas = self.world.gen_future_bounces(self.notes, update_loading_screen)
# No need to fix overlap here, as it's handled in gen_future_bounces
except UserCancelsLoadingError:
return True
except MapLoadingFailureError as e:
return e.args[0] if len(e.args) else "Big error... can't load map :("
self.world.start_time = get_current_time()
self.world.square.dir = [0, 0]
if self.world.future_bounces:
self.world.square.pos = self.world.future_bounces[0].square_pos
else:
self.world.square.pos = [Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 2]
return None
def draw(self, screen: pygame.Surface):
if not self.active:
return
if not self.music_has_played:
if not self.offset_happened:
for bnc_change in self.world.future_bounces:
bnc_change.time += Config.start_playing_delay / 1000
self.offset_happened = True
if self.world.time-Config.music_offset/1000 > Config.start_playing_delay/1000:
self.music_has_played = True
song_load_before = get_current_time()
pygame.mixer.music.play()
for bnc_change in self.world.future_bounces:
bnc_change.time += (get_current_time()-song_load_before)/1000
screen_rect = screen.get_rect()
# Set world time
self.world.update_time()
# Move camera
self.camera.attempt_movement()
# Handle square bounces
self.world.handle_bouncing(self.world.square)
# Move square
self.world.square.reg_move()
# Square in center of camera if locked
if self.camera.locked_on_square:
self.camera.follow(self.world.square)
# Bounce animation
sqrect = self.camera.offset(self.world.square.rect)
if (self.world.time - 0.25) + Config.music_offset / 1000 < self.world.square.last_bounce_time:
lerp = abs((self.world.time - 0.25 + Config.music_offset / 1000) - self.world.square.last_bounce_time) * 5
lerp = lerp ** 2 # Square it for better-looking interpolation
if self.world.square.latest_bounce_direction:
sqrect.inflate_ip((lerp * 5, -10 * lerp))
else:
sqrect.inflate_ip((-10 * lerp, lerp * 5))
# Draw background (walls)
screen.fill(get_colors()["background"])
# Create a hallway surface
hallway_surface = pygame.Surface((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT), pygame.SRCALPHA)
# Safe areas
for safe_area in self.world.safe_areas:
offsetted = self.camera.offset(safe_area)
if screen_rect.colliderect(offsetted):
pygame.draw.rect(hallway_surface, get_colors()["hallway"], offsetted)
# Obtain current time
current_time = pygame.time.get_ticks() / 1000.0 # Time in seconds
# Draw stars onto the hallway surface
for star in self.world.stars:
star.update(current_time) # Update blinking effect
screen_pos = star.get_screen_pos(self.camera)
if 0 <= screen_pos[0] < Config.SCREEN_WIDTH and 0 <= screen_pos[1] < Config.SCREEN_HEIGHT:
# Check if star's position is within the hallway (safe areas)
if hallway_surface.get_at((int(screen_pos[0]), int(screen_pos[1]))).a != 0:
pygame.draw.circle(hallway_surface, star.color, (int(screen_pos[0]), int(screen_pos[1])), int(star.size))
# Blit the hallway surface onto the main screen
screen.blit(hallway_surface, (0, 0))
# Draw pegs
for peg in self.world.pegs:
peg.draw(screen, self.camera, self.world.time)
# Particles
for particle in self.world.particles:
pygame.draw.rect(screen, particle.color, self.camera.offset(particle.rect))
for remove_particle in [particle for particle in self.world.particles if particle.age()]:
self.world.particles.remove(remove_particle)
# Particle trail in game
if Config.particle_trail:
# Add particles periodically
if not self.world.square.died:
if TRAIL_COLOR_BASED_ON_SQUARE:
square_color = self.world.square.get_current_color()
color = square_color
else:
color = (255, 255, 255)
new = Particle(self.world.square.pos.copy(), [0, 0], True, color=color)
new.delta = [random.randint(-2, 2)/20, random.randint(5, 15)/20]
self.world.particles.append(new)
# Draw square
self.world.square.draw(screen, sqrect)
def handle_event(self, event: pygame.event.Event):
if not self.active:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
if event.key == pygame.K_TAB:
self.camera.locked_on_square = not self.camera.locked_on_square
class InvalidSongError(Exception):
"""The song is invalid somehow"""
pass
def make_song_from_midi(fpath: str) -> Any:
new_song = type('Song', (object,), {})()
new_song.fp = fpath
new_song.name = os.path.basename(fpath) # Use filename as song name
new_song.creator = '' # Empty creator
new_song.music_offset = 0
return new_song
def main():
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT), pygame.DOUBLEBUF | pygame.HWSURFACE)
pygame.display.set_caption("chubbMidi")
try:
pygame.display.set_icon(pygame.image.load(Config.assets).convert_alpha())
except Exception as e:
print(e)
# Specify the song file to use
selected_song_filepath = join(Config.song, SONG)
if not isfile(selected_song_filepath):
print(f"Song file {selected_song_filepath} not found.")
return
Config.current_song = make_song_from_midi(selected_song_filepath)
# Load the music file
try:
pygame.mixer.music.load(Config.current_song.fp)
except Exception as e:
print(f"Error loading MIDI file as audio: {e}")
print("Your platform may not support MIDI playback via pygame.mixer.music.")
print("The game will run without audio.")
# Uncomment the line below to exit if audio is essential
# return
pygame.mixer.music.set_volume(Config.volume / 100)
# Initialize the game
game = Game()
# Start the game
if msg := game.start_song(screen):
if isinstance(msg, str):
print(f"Error starting game: {msg}")
return
game.active = True
running = True
while running:
# screen.fill(get_colors()["background"]) # Moved to the draw function
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
game.handle_event(event)
game.draw(screen)
pygame.display.flip()
clock.tick(FRAMERATE)
Config.dt = clock.get_time() / 1000.0 if clock.get_time() > 0 else 0.016 # Avoid division by zero
pygame.quit()
if __name__ == '__main__':
main()ASSISTANT
To implement the heat level feature that adjusts the square's glow intensity based on the speed of collisions, we'll make several modifications to the script while ensuring that existing features remain unaffected.
**1. Add a Boolean Setting for Heat Level at the Top of the Script**
```python
# Heat Level Setting
HEAT_LEVEL = True
```
**2. Update the `Config` Class to Include Heat Level Settings**
We'll add the heat level parameters to the `Config` class for easy adjustments.
```python
class Config:
# ... existing code ...
# Glow effect settings for square
square_glow = SQUARE_GLOW
square_glow_duration = 0.2
glow_intensity = 15 # Adjusted intensity for better visibility
square_min_glow = 0 # Lower value for slight glow when no collision
# Heat Level Settings
HEAT_LEVEL = HEAT_LEVEL # From the top setting
heat_increase_scale = 1.0 # Adjust this to scale the heat increase per collision
max_heat_increase = 5.0 # Max heat level increase per collision
max_heat_level = 10.0 # Max heat level
heat_decay_rate = 1.0 # Heat level decays at 1 unit per second
# ... existing code ...
```
**3. Modify the `Square` Class to Handle Heat Level Calculations**
- **Initialize `heat_level` and `last_collision_interval` in `__init__`:**
```python
class Square:
def __init__(self, x: float = 0, y: float = 0, dx: int = 1, dy: int = 1):
self.pos: list[float] = [x, y]
self.dir: list[int] = [dx, dy]
self.last_bounce_time = -100
self.latest_bounce_direction = 0 # 0 = horiz, 1 = vert
self.past_colors = []
self.died = False
self.time_since_glow_start = -1000 # For glow effect
self.last_collision_interval = None
self.heat_level = 0
```
- **Update Heat Level on Collision in `obey_bounce`:**
```python
def obey_bounce(self, bounce: Bounce):
# Before updating the last_bounce_time, compute time since last collision
if self.last_bounce_time != -100:
self.last_collision_interval = bounce.time - self.last_bounce_time
# Update heat level
if Config.HEAT_LEVEL and self.last_collision_interval > 0:
heat_increase = Config.heat_increase_scale / self.last_collision_interval
heat_increase = min(heat_increase, Config.max_heat_increase)
self.heat_level += heat_increase
self.heat_level = min(self.heat_level, Config.max_heat_level)
else:
self.last_collision_interval = None # First collision, no previous interval
# Then update last_bounce_time
self.last_bounce_time = bounce.time
# Start glow effect
self.time_since_glow_start = pygame.time.get_ticks()
```
- **Decay Heat Level Over Time in `update_heat_level`:**
```python
def update_heat_level(self, dt):
if Config.HEAT_LEVEL:
self.heat_level = max(0, self.heat_level - Config.heat_decay_rate * dt)
```
- **Adjust Glow Intensity Based on Heat Level in `draw_glowing3`:**
```python
def draw_glowing3(self, win, rect):
if self.died:
return
if Config.square_glow:
if Config.HEAT_LEVEL:
val = Config.square_min_glow + (self.heat_level / Config.max_heat_level) * (Config.glow_intensity - Config.square_min_glow)
val = int(val)
val = min(val, Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
# Original glow code
current_ticks = pygame.time.get_ticks()
time_since_glow = current_ticks - self.time_since_glow_start
if EMPTY_SQUARE:
# Glow is always slightly on, increases during collision
if time_since_glow < Config.square_glow_duration * 1000:
progress = 1 - (time_since_glow) / (Config.square_glow_duration * 1000)
val = int(progress * Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
# Minimum glow intensity when not colliding
val = Config.square_min_glow
else:
# Glow only during collision
if time_since_glow < Config.square_glow_duration * 1000:
progress = 1 - (time_since_glow) / (Config.square_glow_duration * 1000)
val = int(progress * Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
return # Do not draw glow if not within glow duration
if val > 0:
surf = self.compute_glowy_surface(rect, val)
win.blit(surf, rect.move(-40, -40).topleft, special_flags=pygame.BLEND_RGBA_ADD)
```
**4. Update the Game Loop to Call `update_heat_level`**
In the `Game` class's `draw` method, after updating the world time, we'll call `update_heat_level` with the current delta time.
```python
def draw(self, screen: pygame.Surface):
if not self.active:
return
if not self.music_has_played:
if not self.offset_happened:
for bnc_change in self.world.future_bounces:
bnc_change.time += Config.start_playing_delay / 1000
self.offset_happened = True
if self.world.time-Config.music_offset/1000 > Config.start_playing_delay/1000:
self.music_has_played = True
song_load_before = get_current_time()
pygame.mixer.music.play()
for bnc_change in self.world.future_bounces:
bnc_change.time += (get_current_time()-song_load_before)/1000
screen_rect = screen.get_rect()
# Set world time
self.world.update_time()
# Update heat level
self.world.square.update_heat_level(Config.dt)
# ... existing code ...
```
**5. Ensure Existing Features Remain Unaffected**
All changes are additive and conditional on the `HEAT_LEVEL` setting. Existing features and configurations remain intact, and the new heat level feature integrates smoothly without interfering with other functionalities.
**6. Test and Adjust Parameters as Needed**
After implementing, you may need to adjust the values of the following parameters in the `Config` class to achieve the desired smoothness and responsiveness:
- `heat_increase_scale`
- `max_heat_increase`
- `max_heat_level`
- `heat_decay_rate`
**Complete Modified Code**
```python
import pygame
import mido
import sys
import random
import requests
import colorsys
import os
import math
import cv2
import numpy as np
from os.path import join, isfile
from time import time as get_current_time
from typing import Union, Optional, Any
from enum import Enum
from math import sin, pi
pygame.mixer.init()
pygame.init()
FRAMERATE = 60
# =========================
# Customizable Configuration
# =========================
# Midi Selection
SONG = "rush-e.mid"
# Screen Resolution
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
# Other Customizable Settings
SQUARE_SPEED = 600
SQUARE_SIZE = 40
BOUNCE_MIN_SPACING = 50
DIRECTION_CHANGE = 30
EMPTY_SQUARE = True
# Particle Settings
PARTICLE_BOUNCE = False
PARTICLE_TRAIL = False
TRAIL_COLOR_BASED_ON_SQUARE = False
PARTICLE_SPEED = 5
# Peg Settings
BOUNCE_PEG = True
PEG_COLOR = (255, 255, 255)
PEG_REMOVE = True
PEG_MULTI_COLOR = False
NO_COLOR = False
# Glow Settings
SQUARE_GLOW = True
PEG_GLOW = True
GLOW_RANDOM_COLOR = False
# Star Settings
STAR = True
STAR_COLOR = (255, 255, 255)
STAR_DENSITY = 0.0001
# Heat Level Setting
HEAT_LEVEL = True
# =========================
def color_hunt():
def is_bright(hex_color):
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
luminance = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]
r, g, b = [x / 255.0 for x in rgb]
h, s, v = colorsys.rgb_to_hsv(r, g, b)
return luminance >= 130 and s >= 0.3
while True:
url = 'https://colorhunt.co/php/feed.php'
headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
data = {
'step': 0,
'sort': 'random',
'tags': ''
}
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
palettes = response.json()
color_pairs = []
for palette in palettes:
code = palette['code']
hex_colors = [f'#{code[i:i+6].upper()}' for i in range(0, len(code), 6)]
if len(hex_colors) == 4 and all(is_bright(hex_color) for hex_color in hex_colors):
color_pairs.append(tuple(hex_colors))
if color_pairs:
selected_color_pair = random.choice(color_pairs)
return selected_color_pair
else:
raise Exception(f"Failed to retrieve data. Status code: {response.status_code}")
selected_colors = color_hunt()
color1, color2, color3, color4 = selected_colors
class Config:
# Constants
SQUARE_SIZE = SQUARE_SIZE
PARTICLE_SPEED = PARTICLE_SPEED
CAMERA_SPEED = 500
# Colors
color_themes = {
"dark": {
"hallway": pygame.Color('#2D033B'),
"background": pygame.Color('#070F2B'),
"square": [
pygame.Color(color1),
pygame.Color(color2),
pygame.Color(color3),
pygame.Color(color4)
],
"peg": pygame.Color(PEG_COLOR), # Added peg color
}
}
# Configuration
script_dir = os.path.dirname(os.path.abspath(__file__))
assets = os.path.join(script_dir, 'assets/icon.png')
song = os.path.join(script_dir, 'songs')
font = os.path.join(script_dir, 'assets/poppins-regular.ttf')
SCREEN_WIDTH = SCREEN_WIDTH
SCREEN_HEIGHT = SCREEN_HEIGHT
theme: Optional[str] = "dark"
seed: Optional[int] = None
camera_mode: Optional[int] = 2
start_playing_delay = 5000
max_notes: Optional[int] = None
bounce_min_spacing: Optional[float] = BOUNCE_MIN_SPACING
square_speed: Optional[int] = SQUARE_SPEED
volume: Optional[int] = 100
music_offset: Optional[int] = 0
direction_change_chance: Optional[int] = DIRECTION_CHANGE
particle_trail = PARTICLE_TRAIL
do_color_bounce_pegs = BOUNCE_PEG
do_particles_on_bounce = PARTICLE_BOUNCE
# Non-configurable settings
backtrack_chance: Optional[float] = 0.02
backtrack_amount: Optional[int] = 40
square_swipe_anim_speed: Optional[int] = 4
particle_amount = 20
# Glow effect settings for square
square_glow = SQUARE_GLOW
square_glow_duration = 0.2
glow_intensity = 15 # Adjusted intensity for better visibility
square_min_glow = 0 # Lower value for slight glow when no collision
# Heat Level Settings
HEAT_LEVEL = HEAT_LEVEL # From the top setting
heat_increase_scale = 1.0 # Adjust this to scale the heat increase per collision
max_heat_increase = 5.0 # Max heat level increase per collision
max_heat_level = 10.0 # Max heat level
heat_decay_rate = 1.0 # Heat level decays at 1 unit per second
# Glow effect settings for pegs
peg_glow = PEG_GLOW
peg_glow_duration = 0.5
peg_glow_intensity = 15 # Adjusted intensity for better visibility
peg_min_glow = 7
if GLOW_RANDOM_COLOR:
peg_glow_color = pygame.Color(random.choice(selected_colors))
square_glow_color = pygame.Color(random.choice(selected_colors))
else:
peg_glow_color = pygame.Color(255, 255, 255)
square_glow_color = pygame.Color(255, 255, 255)
# Star settings
star_color = STAR_COLOR
star_density = STAR_DENSITY
# Other random stuff
current_song = None
dt = 0.01
def get_colors():
return Config.color_themes.get(Config.theme, Config.color_themes["dark"])
def read_midi_file(file):
midi_file = mido.MidiFile(file=file)
notes = []
current_time = 0
for msg in midi_file:
if msg.type == 'note_on' and msg.velocity != 0:
timestamp = current_time + msg.time
notes.append(round(timestamp*1000)/1000)
current_time += msg.time
return notes
def remove_too_close_values(lst: list[float], threshold=30) -> list[float]:
"""Assumes the list is sorted"""
new = []
before = None
for _ in lst:
if before is None:
before = _
new.append(_)
continue
if before+threshold/1000 > _:
continue
before = _
new.append(_)
return new
def fix_overlap(rects: list[pygame.Rect], callback=None):
"""
Optimized function to merge overlapping rectangles efficiently.
"""
if callback is None:
callback = lambda _: None
rects = rects.copy()
# Sort rectangles based on x coordinate
rects.sort(key=lambda r: (r.x, r.y))
merged_rects = []
for rect in rects:
if not merged_rects:
merged_rects.append(rect)
else:
last = merged_rects[-1]
if last.colliderect(rect):
merged_rect = last.union(rect)
merged_rects[-1] = merged_rect
else:
merged_rects.append(rect)
callback("Finished merging rectangles")
return merged_rects
def get_font(size: int = 24) -> pygame.font.Font:
font_path = Config.font
return pygame.font.Font(font_path, size)
class MapLoadingFailureError(Exception):
"""The map fails to load (recurs function fails)"""
pass
class UserCancelsLoadingError(Exception):
"""User cancels the loading screen"""
pass
def interpolate_fn(n):
"""Interpolate sigmoidally from 0-1"""
n = min(max(n, 0), 1)
return sin(pi * (n - 0.5)) / 2 + 0.5
class Particle:
SPEED_VARIATION = 4
SIZE_MIN = 7
SIZE_MAX = 14
AGE_RATE = 20
SLOW_DOWN_RATE = 1.2
def __init__(self, pos: list[float], delta: list[float], invert_color: bool = False, color: Optional[tuple] = None):
self.pos = pos.copy()
self.size = random.randint(Particle.SIZE_MIN, Particle.SIZE_MAX)
self.delta = delta.copy()
self.delta[0] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
self.delta[1] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
if color is not None:
self.color = color
else:
# Default color if color is not given
self.color = get_colors()["hallway"] if not invert_color else get_colors()["background"]
# Adjust color to white for particles
self.color = (255, 255, 255)
def age(self):
self.size -= Particle.AGE_RATE*Config.dt
self.x += self.delta[0] * Config.PARTICLE_SPEED
self.y += self.delta[1] * Config.PARTICLE_SPEED
if Config.dt != 0:
self.delta[0] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
self.delta[1] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
return self.size <= 0
@property
def x(self):
return self.pos[0]
@x.setter
def x(self, val: float):
self.pos[0] = val
@property
def y(self):
return self.pos[1]
@y.setter
def y(self, val: float):
self.pos[1] = val
@property
def rect(self):
return pygame.Rect(self.x-self.size/2, self.y-self.size/2, *(2*[self.size]))
class Bounce:
def __init__(self, sq_pos: list[float], sq_dir: list[int], time: float, bounce_dir: int):
self.square_pos = sq_pos # New square position
self.square_dir = sq_dir # New square direction
self.bounce_dir = bounce_dir # Bounce direction for squish effect; 0 or 1
self.time = time # Time during bounce
def get_collision_rect(self):
sx, sy = self.square_pos
if self.bounce_dir == 0:
# Bounce left or right wall
if self.square_dir[0] == -1:
# Right wall
return pygame.Rect(
sx+Config.SQUARE_SIZE/2+1,
sy-10,
10,
20
)
elif self.square_dir[0] == 1:
# Left wall
return pygame.Rect(
sx-10-Config.SQUARE_SIZE/2-1,
sy-10,
10,
20
)
elif self.bounce_dir == 1:
# Bounce top or bottom wall
if self.square_dir[1] == -1:
# Bottom wall
return pygame.Rect(
sx-10,
sy+Config.SQUARE_SIZE/2+1,
20,
10
)
elif self.square_dir[1] == 1:
# Top wall
return pygame.Rect(
sx-10,
sy-10-Config.SQUARE_SIZE/2-1,
20,
10
)
def copy(self) -> "Bounce":
return Bounce(self.square_pos.copy(), self.square_dir.copy(), self.time, self.bounce_dir)
def __repr__(self):
return f"<Bounce(sq_pos={self.square_pos}, sq_dir={self.square_dir}, time={self.time}, dir={self.bounce_dir})"
# Glow effect functions
def create_border(image: np.ndarray, margin: int, thickness: int, color: tuple) -> np.ndarray:
height, width = image.shape[:2]
cv2.rectangle(image, (margin, margin), (width - margin, height - margin), color, thickness=thickness)
return image
def apply_blooming(image: np.ndarray) -> np.ndarray:
# Provide some blurring to image, to create some bloom.
cv2.GaussianBlur(image, ksize=(15, 15), sigmaX=20, sigmaY=20, dst=image)
cv2.blur(image, ksize=(12, 12), dst=image)
return image
def glowing_border(image: np.ndarray, margin=20, thickness=20, color=(255, 255, 255)):
"""
Create a glowing border around an image.
"""
# Generate the colored border.
image = create_border(image, margin, thickness, color)
# Apply the bloom effect to the image with the border.
image = apply_blooming(image)
# Reassert the original border to enhance the edges.
image = create_border(image, margin - 1, 1, color)
image = create_border(image, margin + 1, 1, color)
return image
def make_glowy2(size, color, intensity=0) -> pygame.Surface:
image = np.zeros((*size[::-1], 3), dtype=np.uint8)
border_color_rgb = color # Assuming color is RGB tuple
border = glowing_border(image.copy(), color=border_color_rgb, thickness=intensity)
return pygame.surfarray.make_surface(np.fliplr(np.rot90(border, k=-1)))
class Square:
glowing_surface_cache = {}
def __init__(self, x: float = 0, y: float = 0, dx: int = 1, dy: int = 1):
self.pos: list[float] = [x, y]
self.dir: list[int] = [dx, dy]
self.last_bounce_time = -100
self.latest_bounce_direction = 0 # 0 = horiz, 1 = vert
self.past_colors = []
self.died = False
self.time_since_glow_start = -1000 # For glow effect
self.last_collision_interval = None
self.heat_level = 0
def register_past_color(self, col: tuple[int, int, int]):
for _ in range(max(Config.square_swipe_anim_speed, 1)):
self.past_colors.insert(0, col)
while len(self.past_colors) > Config.SQUARE_SIZE * 4 / 5:
self.past_colors.pop()
def get_surface(self, size: tuple[int, int]):
ss = int(Config.SQUARE_SIZE * 4 / 5)
surf = pygame.Surface((ss, ss))
for index, col in enumerate(self.past_colors):
y = index if self.dir_y != 1 else ss - 1 - index
pygame.draw.line(surf, col, (0, y), (ss, y))
return pygame.transform.scale(surf, size)
def copy(self) -> "Square":
new = Square(*self.pos, *self.dir)
new.last_bounce_time = self.last_bounce_time
new.latest_bounce_direction = self.latest_bounce_direction
return new
def get_current_color(self):
if EMPTY_SQUARE:
return (255, 255, 255) # Border color is white
else:
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
return get_colors()["square"][square_color_index % len(get_colors()["square"])]
def compute_glowy_surface(self, rect, val):
cache_key = (rect.size, val)
if cache_key in Square.glowing_surface_cache:
return Square.glowing_surface_cache[cache_key]
glowy_borders = make_glowy2((rect.width + 40, rect.height + 40), Config.square_glow_color, val)
surface = pygame.Surface(rect.inflate(100, 100).size, pygame.SRCALPHA)
surface.blit(glowy_borders, (20, 20), special_flags=pygame.BLEND_RGBA_ADD)
Square.glowing_surface_cache[cache_key] = surface
return surface
def draw_glowing3(self, win, rect):
if self.died:
return
if Config.square_glow:
if Config.HEAT_LEVEL:
# Glow intensity based on heat level
val = Config.square_min_glow + (self.heat_level / Config.max_heat_level) * (Config.glow_intensity - Config.square_min_glow)
val = int(val)
val = min(val, Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
current_ticks = pygame.time.get_ticks()
time_since_glow = current_ticks - self.time_since_glow_start
if EMPTY_SQUARE:
# Glow is always slightly on, increases during collision
if time_since_glow < Config.square_glow_duration * 1000:
progress = 1 - (time_since_glow) / (Config.square_glow_duration * 1000)
val = int(progress * Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
# Minimum glow intensity when not colliding
val = Config.square_min_glow
else:
# Glow only during collision
if time_since_glow < Config.square_glow_duration * 1000:
progress = 1 - (time_since_glow) / (Config.square_glow_duration * 1000)
val = int(progress * Config.glow_intensity)
val = max(val, Config.square_min_glow)
else:
return # Do not draw glow if not within glow duration
if val > 0:
surf = self.compute_glowy_surface(rect, val)
win.blit(surf, rect.move(-40, -40).topleft, special_flags=pygame.BLEND_RGBA_ADD)
@property
def x(self):
return self.pos[0]
@property
def y(self):
return self.pos[1]
def draw(self, screen: pygame.Surface, sqrect: pygame.Rect):
if self.died:
return
# Draw glowing square if glowing
self.draw_glowing3(screen, sqrect)
if EMPTY_SQUARE:
# Draw the square as an empty square with white border
border_color = (255, 255, 255)
border_thickness = 2 # Adjust as needed
pygame.draw.rect(screen, border_color, sqrect, border_thickness)
else:
# Default drawing of the square
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
self.register_past_color(get_colors()["square"][square_color_index % len(get_colors()["square"])])
# Draw the square itself
pygame.draw.rect(screen, (0, 0, 0), sqrect)
sq_surf = self.get_surface(
tuple(sqrect.inflate(-int(Config.SQUARE_SIZE / 5), -int(Config.SQUARE_SIZE / 5))[2:]))
screen.blit(sq_surf, sq_surf.get_rect(center=sqrect.center))
@x.setter
def x(self, val: int):
self.pos[0] = val
@y.setter
def y(self, val: int):
self.pos[1] = val
@property
def dir_x(self):
return self.dir[0]
@property
def dir_y(self):
return self.dir[1]
@property
def rect(self):
return pygame.Rect(self.x - Config.SQUARE_SIZE / 2, self.y - Config.SQUARE_SIZE / 2,
*([Config.SQUARE_SIZE] * 2))
def obey_bounce(self, bounce: Bounce):
# Before updating the last_bounce_time, compute time since last collision
if self.last_bounce_time != -100:
self.last_collision_interval = bounce.time - self.last_bounce_time
# Update heat level
if Config.HEAT_LEVEL and self.last_collision_interval > 0:
heat_increase = Config.heat_increase_scale / self.last_collision_interval
heat_increase = min(heat_increase, Config.max_heat_increase)
self.heat_level += heat_increase
self.heat_level = min(self.heat_level, Config.max_heat_level)
else:
self.last_collision_interval = None # First collision, no previous interval
# Then update last_bounce_time
self.last_bounce_time = bounce.time
self.pos = bounce.square_pos.copy()
self.dir = bounce.square_dir.copy()
self.latest_bounce_direction = bounce.bounce_dir
# Start glow effect
self.time_since_glow_start = pygame.time.get_ticks()
return
def update_heat_level(self, dt):
if Config.HEAT_LEVEL:
self.heat_level = max(0, self.heat_level - Config.heat_decay_rate * dt)
def reg_move(self, use_dt: bool = True):
self.x += self.dir_x * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
self.y += self.dir_y * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
class Camera:
def __init__(self, x: int = 0, y: int = 0):
self.x = x
self.y = y
self.locked_on_square = True
self.lock_type: CameraFollow = CameraFollow(Config.camera_mode)
def attempt_movement(self):
if not self.locked_on_square:
keys = pygame.key.get_pressed()
shift_modifier = (keys[pygame.K_LSHIFT] | keys[pygame.K_RSHIFT]) + 1
self.x += (keys[pygame.K_d] - keys[pygame.K_a]) * Config.CAMERA_SPEED * shift_modifier / FRAMERATE
self.y += (keys[pygame.K_s] - keys[pygame.K_w]) * Config.CAMERA_SPEED * shift_modifier / FRAMERATE
@property
def pos(self):
return self.x, self.y
@pos.setter
def pos(self, val: Union[tuple[int, int], list[int]]):
self.x, self.y = val
def offset(self, pos_or_rect: Union[pygame.Rect, tuple[int, int]]) -> Union[pygame.Rect, list[int]]:
if isinstance(pos_or_rect, pygame.Rect):
return pos_or_rect.move(-self.x, -self.y)
else:
return [pos_or_rect[0]-self.x, pos_or_rect[1]-self.y]
def apply_parallax(self, pos: tuple[float, float], depth: float) -> list[float]:
# Parallax factor: nearer stars move faster than distant ones
parallax_factor = 0.2 + depth * 0.8 # From 0.2 to 1.0
screen_x = (pos[0] - self.x) * parallax_factor
screen_y = (pos[1] - self.y) * parallax_factor
return [screen_x, screen_y]
def follow(self, square: Square):
# Square in center
if self.lock_type == CameraFollow.Center:
self.pos = [square.x - Config.SCREEN_WIDTH / 2, square.y - Config.SCREEN_HEIGHT / 2]
# Smooth camera
if self.lock_type == CameraFollow.Smoothed:
easing_rate = 3
self.x = (square.x - Config.SCREEN_WIDTH / 2) * easing_rate * Config.dt + self.x - easing_rate * self.x * Config.dt
self.y = (square.y - Config.SCREEN_HEIGHT / 2) * easing_rate * Config.dt + self.y - easing_rate * self.y * Config.dt
class CameraFollow(Enum):
Center = 0 # Center the square
Smoothed = 2 # Smoothed camera
class Star:
def __init__(self, pos: tuple[float, float], depth: float):
self.initial_pos = list(pos)
self.depth = depth # between 0 (far) and 1 (near)
size_min = 0.5
size_max = 2
self.size = size_min + (1 - self.depth) * (size_max - size_min)
self.phase = random.uniform(0, 2 * math.pi) # For random phase
self.brightness = 0 # Will be updated in the update method
self.color = Config.star_color # Use star color from Config
def update(self, time):
# Adjust brightness over time to create blinking effect
blink_speed = 10 # can adjust to get desired speed
brightness_variation = (math.sin(blink_speed * time + self.phase) + 1) / 2 # Normalize between 0 and 1
min_brightness = 100
max_brightness = 255
brightness = min_brightness + brightness_variation * (max_brightness - min_brightness)
self.color = tuple(min(255, int(c * (brightness / 255))) for c in Config.star_color) # Adjust color based on brightness
def get_screen_pos(self, camera: Camera):
parallax_factor = 0.2 + self.depth * 0.8 # from 0.2 to 1.0
screen_x = (self.initial_pos[0] - camera.x) * parallax_factor
screen_y = (self.initial_pos[1] - camera.y) * parallax_factor
return [screen_x, screen_y]
class Peg:
glowing_surface_cache = {}
def __init__(self, rect: pygame.Rect, color: tuple, collision_time: float):
self.rect = rect
self.color = color
self.collision_time = collision_time # The time when the square is supposed to bounce on this peg
self.time_since_glow_start = -1000 # For glow effect
self.visible = False # Visibility flag
def update_glow(self, current_time):
# Update the glow start time
self.time_since_glow_start = current_time
self.visible = True # Set visibility to True after collision
def compute_glowy_surface(self, rect, val):
cache_key = (rect.size, val)
if cache_key in Peg.glowing_surface_cache:
return Peg.glowing_surface_cache[cache_key]
glowy_borders = make_glowy2((rect.width + 40, rect.height + 40), Config.peg_glow_color, val)
surface = pygame.Surface(rect.inflate(100, 100).size, pygame.SRCALPHA)
surface.blit(glowy_borders, (20, 20), special_flags=pygame.BLEND_RGBA_ADD)
Peg.glowing_surface_cache[cache_key] = surface
return surface
def draw_glow(self, screen, rect):
if Config.peg_glow:
current_ticks = pygame.time.get_ticks()
elapsed_time = (current_ticks - self.time_since_glow_start) / 1000.0
if elapsed_time < Config.peg_glow_duration:
progress = 1 - (elapsed_time) / Config.peg_glow_duration
val = int(progress * Config.peg_glow_intensity)
val = max(val, Config.peg_min_glow)
surf = self.compute_glowy_surface(rect, val)
screen.blit(surf, rect.move(-40, -40).topleft, special_flags=pygame.BLEND_RGBA_ADD)
def draw(self, screen: pygame.Surface, camera: Camera, current_time: float):
offsetted = camera.offset(self.rect)
if screen.get_rect().colliderect(offsetted):
if self.visible:
# After collision time, draw the peg and glow effect
self.draw_glow(screen, offsetted)
pygame.draw.rect(screen, self.color, offsetted)
else:
color = (get_colors()["background"])
pygame.draw.rect(screen, color, offsetted)
pass # Peg remains invisible before collision
class World:
"""It's a cruel world out there"""
def __init__(self):
self.future_bounces: list[Bounce] = []
self.past_bounces: list[Bounce] = []
self.start_time = 0
self.time = 0
self.pegs: list[Peg] = [] # List of Peg objects
self.particles: list[Particle] = []
self.stars: list[Star] = []
self.timestamps = []
self.square = Square()
self.safe_areas: list[pygame.Rect] = []
def update_time(self) -> None:
self.time = get_current_time() - self.start_time
def get_next_bounce(self) -> Bounce:
"""Also pops the bounce from the future_bounces list"""
bounce = self.future_bounces.pop(0)
self.past_bounces.append(bounce)
return bounce
def add_bounce_particles(self, sp: list[float], sd: list[float], color: tuple):
for _ in range(Config.particle_amount):
new = Particle([sp[0] + random.randint(-10, 10), sp[1] + random.randint(-10, 10)], sd, color=color)
self.particles.append(new)
def handle_bouncing(self, square: Square):
if len(self.future_bounces):
if (self.time * 1000 + Config.music_offset)/1000 > self.future_bounces[0].time:
current_bounce = self.get_next_bounce()
before = square.dir.copy()
square.obey_bounce(current_bounce)
changed = square.dir.copy()
for _ in range(2):
if before[_] == changed[_]:
changed[_] = 0
else:
changed[_] = -changed[_]
if Config.do_particles_on_bounce:
square_color = square.get_current_color()
self.add_bounce_particles(square.pos, changed, color=square_color)
# Stop square at end
if len(self.future_bounces) == 0:
square.dir = [0, 0]
square.pos = current_bounce.square_pos
# Update the corresponding peg's glow
index = len(self.past_bounces) - 1 # Index of the current bounce
if index < len(self.pegs):
peg = self.pegs[index]
peg.update_glow(pygame.time.get_ticks())
def generate_stars(self):
self.stars = []
# Calculate total safe area
total_safe_area = sum(area.width * area.height for area in self.safe_areas)
star_density = Config.star_density # Use star density from Config
total_num_stars = int(total_safe_area * star_density)
min_distance_squared = (20) ** 2 # Avoid clustering
# Get bounding rectangle of all safe areas
min_x = min(area.left for area in self.safe_areas)
max_x = max(area.right for area in self.safe_areas)
min_y = min(area.top for area in self.safe_areas)
max_y = max(area.bottom for area in self.safe_areas)
bounding_rect = pygame.Rect(min_x, min_y, max_x - min_x, max_y - min_y)
stars = []
attempts = 0
max_attempts = total_num_stars * 10
while len(stars) < total_num_stars and attempts < max_attempts:
x = random.uniform(bounding_rect.left, bounding_rect.right)
y = random.uniform(bounding_rect.top, bounding_rect.bottom)
point = (x, y)
# Ensure point is within safe area
inside_safe_area = any(area.collidepoint(point) for area in self.safe_areas)
if inside_safe_area:
# Ensure minimum distance between stars
if all((star.initial_pos[0] - x) ** 2 + (star.initial_pos[1] - y) ** 2 >= min_distance_squared for star in stars):
depth = random.uniform(0, 1)
star = Star((x, y), depth)
stars.append(star)
attempts += 1
self.stars = stars
def gen_future_bounces(self, _start_notes: list[float], percent_update_callback):
"""Recursive function with optimized overlap checking"""
total_notes = len(_start_notes)
max_percent = 0
path = []
safe_areas = []
force_return = 0
def recurs(
square: Square,
notes: list[float],
bounces_so_far: list[Bounce] = None,
prev_index_priority=None,
t: float = 0,
depth: int = 0
) -> Union[list[Bounce], bool]:
nonlocal force_return, max_percent
if prev_index_priority is None:
prev_index_priority = [0, 1]
if bounces_so_far is None:
bounces_so_far = []
if total_notes:
gone_through_percent = (total_notes-len(notes)) * 100 // total_notes
while gone_through_percent > max_percent:
max_percent += 1
if percent_update_callback(f"{max_percent}% done generating map"):
raise UserCancelsLoadingError()
all_bounce_rects = [_bounc.get_collision_rect() for _bounc in bounces_so_far]
if len(notes) == 0:
return bounces_so_far
path_segment_start = len(path)
start_rect = square.rect.copy()
while True:
t += 1/FRAMERATE
square.reg_move(False)
path.append(square.rect)
if t > notes[0]:
# No collision (we good)
bounce_indexes = prev_index_priority
# Randomly change direction every X% of the time
if random.random() * 100 < Config.direction_change_chance:
bounce_indexes = list(bounce_indexes.__reversed__())
# Add safe area
safe_areas.append(start_rect.union(square.rect))
for direction_to_bounce in bounce_indexes:
square.dir[direction_to_bounce] *= -1
bounces_so_far.append(Bounce(square.pos.copy(), square.dir.copy(), t, direction_to_bounce))
toextend = recurs(
square=square.copy(),
notes=notes[1:],
bounces_so_far=[_b.copy() for _b in bounces_so_far],
t=t,
prev_index_priority=bounce_indexes.copy(),
depth=depth+1
)
if toextend:
return toextend
else:
bounces_so_far = bounces_so_far[:-1]
square.dir[direction_to_bounce] *= -1
# Backtrack if necessary
if force_return:
force_return -= 1
while len(path) != path_segment_start:
path.pop()
return False
continue
while len(path) != path_segment_start:
path.pop()
return False
othercheck = False
if len(bounces_so_far):
othercheck = bounces_so_far[-1].get_collision_rect().collidelist(path[:-10])+1
if square.rect.collidelist(all_bounce_rects) != -1 or othercheck:
if depth > 200:
if random.random() < Config.backtrack_chance:
max_percent -= (Config.backtrack_amount * 100 // total_notes) + 1
force_return = Config.backtrack_amount
while len(path) != path_segment_start:
path.pop()
return False
_start_notes = _start_notes[:Config.max_notes] if Config.max_notes is not None else _start_notes
self.future_bounces = recurs(
square=self.square.copy(),
notes=remove_too_close_values(
[_sn for _sn in _start_notes],
threshold=Config.bounce_min_spacing
)
)
if self.future_bounces is False:
raise MapLoadingFailureError("The map failed to generate because of the recursion function. " +
"If the MIDI has too many notes too close, it may not generate. " +
"Maybe try changing the \"bounce min spacing\", \"square speed\", or \"change dir chance\" in the config.")
if len(self.future_bounces) == 0:
raise MapLoadingFailureError("Map safe area list is empty. Please report this issue.")
percent_update_callback("Merging overlapping safe areas")
# Merging overlapping safe areas more efficiently
self.safe_areas = fix_overlap(safe_areas, percent_update_callback)
# Generate colors for pegs
def gather_colors(num_colors=20):
colors = []
while len(colors) < num_colors:
new_colors = color_hunt()
rgb_colors = [tuple(int(hex_color[i:i+2], 16) for i in (1, 3, 5)) for hex_color in new_colors]
colors.extend(rgb_colors)
return colors[:num_colors]
collected_colors = gather_colors()
# Setting random colors for the generated pegs
colors = [random.choice(collected_colors) for _ in self.future_bounces]
if not PEG_REMOVE:
if PEG_MULTI_COLOR:
collected_colors = gather_colors()
# Setting random colors for the generated pegs
colors = [random.choice(collected_colors) for _ in self.future_bounces]
elif NO_COLOR:
colors = [get_colors()["background"] for _ in self.future_bounces]
else:
# Use the peg color from get_colors() if peg_multi_color is False
colors = [get_colors()["peg"] for _ in self.future_bounces]
else:
colors = [get_colors()["hallway"] for _ in self.future_bounces]
# Create pegs
self.pegs = []
for i in range(len(self.future_bounces)):
bounce_rect = self.future_bounces[i].get_collision_rect()
color = colors[i]
collision_time = self.future_bounces[i].time
peg = Peg(bounce_rect, color, collision_time)
self.pegs.append(peg)
# Generate stars in safe areas
if STAR:
self.generate_stars()
return self.safe_areas
class Game:
def __init__(self):
self.active = False
self.notes = []
self.camera = Camera()
self.world = World()
self.safe_areas: list[pygame.Rect] = []
self.music_has_played = False
self.offset_happened = False
def start_song(self, screen: pygame.Surface):
random.seed(Config.seed)
# Load song and notes
with open(Config.current_song.fp, 'rb') as f:
notes = read_midi_file(file=f)
notes = sorted([note for note in notes])
self.notes = notes
# Other settings
self.world = World()
self.music_has_played = False
self.offset_happened = False
self.camera.lock_type = CameraFollow(Config.camera_mode)
screen.fill(get_colors()["background"])
pygame.display.flip()
def update_loading_screen(message: str):
screen.fill(get_colors()["background"], pygame.Rect(0, 0, Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT))
font_size = int(Config.SCREEN_HEIGHT * 0.05)
loading_text = get_font(font_size).render(message, True, get_colors()["hallway"])
text_rect = loading_text.get_rect()
text_rect.center = (Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2)
screen.blit(loading_text, text_rect)
pygame.display.flip()
try:
self.safe_areas = self.world.gen_future_bounces(self.notes, update_loading_screen)
# No need to fix overlap here, as it's handled in gen_future_bounces
except UserCancelsLoadingError:
return True
except MapLoadingFailureError as e:
return e.args[0] if len(e.args) else "Big error... can't load map :("
self.world.start_time = get_current_time()
self.world.square.dir = [0, 0]
if self.world.future_bounces:
self.world.square.pos = self.world.future_bounces[0].square_pos
else:
self.world.square.pos = [Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 2]
return None
def draw(self, screen: pygame.Surface):
if not self.active:
return
if not self.music_has_played:
if not self.offset_happened:
for bnc_change in self.world.future_bounces:
bnc_change.time += Config.start_playing_delay / 1000
self.offset_happened = True
if self.world.time-Config.music_offset/1000 > Config.start_playing_delay/1000:
self.music_has_played = True
song_load_before = get_current_time()
pygame.mixer.music.play()
for bnc_change in self.world.future_bounces:
bnc_change.time += (get_current_time()-song_load_before)/1000
screen_rect = screen.get_rect()
# Set world time
self.world.update_time()
# Update heat level
self.world.square.update_heat_level(Config.dt)
# Move camera
self.camera.attempt_movement()
# Handle square bounces
self.world.handle_bouncing(self.world.square)
# Move square
self.world.square.reg_move()
# Square in center of camera if locked
if self.camera.locked_on_square:
self.camera.follow(self.world.square)
# Bounce animation
sqrect = self.camera.offset(self.world.square.rect)
if (self.world.time - 0.25) + Config.music_offset / 1000 < self.world.square.last_bounce_time:
lerp = abs((self.world.time - 0.25 + Config.music_offset / 1000) - self.world.square.last_bounce_time) * 5
lerp = lerp ** 2 # Square it for better-looking interpolation
if self.world.square.latest_bounce_direction:
sqrect.inflate_ip((lerp * 5, -10 * lerp))
else:
sqrect.inflate_ip((-10 * lerp, lerp * 5))
# Draw background (walls)
screen.fill(get_colors()["background"])
# Create a hallway surface
hallway_surface = pygame.Surface((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT), pygame.SRCALPHA)
# Safe areas
for safe_area in self.world.safe_areas:
offsetted = self.camera.offset(safe_area)
if screen_rect.colliderect(offsetted):
pygame.draw.rect(hallway_surface, get_colors()["hallway"], offsetted)
# Obtain current time
current_time = pygame.time.get_ticks() / 1000.0 # Time in seconds
# Draw stars onto the hallway surface
for star in self.world.stars:
star.update(current_time) # Update blinking effect
screen_pos = star.get_screen_pos(self.camera)
if 0 <= screen_pos[0] < Config.SCREEN_WIDTH and 0 <= screen_pos[1] < Config.SCREEN_HEIGHT:
# Check if star's position is within the hallway (safe areas)
if hallway_surface.get_at((int(screen_pos[0]), int(screen_pos[1]))).a != 0:
pygame.draw.circle(hallway_surface, star.color, (int(screen_pos[0]), int(screen_pos[1])), int(star.size))
# Blit the hallway surface onto the main screen
screen.blit(hallway_surface, (0, 0))
# Draw pegs
for peg in self.world.pegs:
peg.draw(screen, self.camera, self.world.time)
# Particles
for particle in self.world.particles:
pygame.draw.rect(screen, particle.color, self.camera.offset(particle.rect))
for remove_particle in [particle for particle in self.world.particles if particle.age()]:
self.world.particles.remove(remove_particle)
# Particle trail in game
if Config.particle_trail:
# Add particles periodically
if not self.world.square.died:
if TRAIL_COLOR_BASED_ON_SQUARE:
square_color = self.world.square.get_current_color()
color = square_color
else:
color = (255, 255, 255)
new = Particle(self.world.square.pos.copy(), [0, 0], True, color=color)
new.delta = [random.randint(-2, 2)/20, random.randint(5, 15)/20]
self.world.particles.append(new)
# Draw square
self.world.square.draw(screen, sqrect)
def handle_event(self, event: pygame.event.Event):
if not self.active:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
if event.key == pygame.K_TAB:
self.camera.locked_on_square = not self.camera.locked_on_square
class InvalidSongError(Exception):
"""The song is invalid somehow"""
pass
def make_song_from_midi(fpath: str) -> Any:
new_song = type('Song', (object,), {})()
new_song.fp = fpath
new_song.name = os.path.basename(fpath) # Use filename as song name
new_song.creator = '' # Empty creator
new_song.music_offset = 0
return new_song
def main():
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT), pygame.DOUBLEBUF | pygame.HWSURFACE)
pygame.display.set_caption("chubbMidi")
try:
pygame.display.set_icon(pygame.image.load(Config.assets).convert_alpha())
except Exception as e:
print(e)
# Specify the song file to use
selected_song_filepath = join(Config.song, SONG)
if not isfile(selected_song_filepath):
print(f"Song file {selected_song_filepath} not found.")
return
Config.current_song = make_song_from_midi(selected_song_filepath)
# Load the music file
try:
pygame.mixer.music.load(Config.current_song.fp)
except Exception as e:
print(f"Error loading MIDI file as audio: {e}")
print("Your platform may not support MIDI playback via pygame.mixer.music.")
print("The game will run without audio.")
# Uncomment the line below to exit if audio is essential
# return
pygame.mixer.music.set_volume(Config.volume / 100)
# Initialize the game
game = Game()
# Start the game
if msg := game.start_song(screen):
if isinstance(msg, str):
print(f"Error starting game: {msg}")
return
game.active = True
running = True
while running:
# screen.fill(get_colors()["background"]) # Moved to the draw function
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
game.handle_event(event)
game.draw(screen)
pygame.display.flip()
clock.tick(FRAMERATE)
Config.dt = clock.get_time() / 1000.0 if clock.get_time() > 0 else 0.016 # Avoid division by zero
pygame.quit()
if __name__ == '__main__':
main()
```
This modified script adds the heat level feature as per your requirements, ensuring smooth glow transitions based on the time between collisions, without affecting existing functionalities.