From 74ccf43124f983f5ec411a4971280eceaa917ea2 Mon Sep 17 00:00:00 2001 From: quasar098 <70716985+quasar098@users.noreply.github.com> Date: Thu, 15 Jun 2023 14:09:17 -0400 Subject: [PATCH] 2.0 --- README.md | 15 ++++--- assets/basehiticon.png | Bin 0 -> 152 bytes assets/death.mp3 | Bin 0 -> 34077 bytes assets/early.png | Bin 0 -> 1340 bytes assets/good.png | Bin 0 -> 1750 bytes assets/late.png | Bin 0 -> 1499 bytes assets/miss.png | Bin 0 -> 1736 bytes assets/perfect.png | Bin 0 -> 2422 bytes config.py | 10 ++++- configpage.py | 30 +++++++++++++ game.py | 74 +++++++++++++++++++++--------- hiticon.py | 30 +++++++++++++ keystrokes.py | 29 ++++++++++++ scorekeeper.py | 99 ++++++++++++++++++++++++++--------------- square.py | 3 ++ world.py | 10 ++--- 16 files changed, 229 insertions(+), 71 deletions(-) create mode 100644 assets/basehiticon.png create mode 100644 assets/death.mp3 create mode 100644 assets/early.png create mode 100644 assets/good.png create mode 100644 assets/late.png create mode 100644 assets/miss.png create mode 100644 assets/perfect.png create mode 100644 hiticon.py create mode 100644 keystrokes.py diff --git a/README.md b/README.md index aed4c9e..88de5c8 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,11 @@ cant remember where i got the others from (sorry if i stole midi file) mainmenu.mp3 song is Aerial City by Chika for tetr.io (will replace later, but this is fine for now i think) -if you do not enjoy me using your midi or songs, i can remove it from this project and replace them +death sound is undertale gaster fade + +colors generated using [coolors.co](https://coolors.co/generate) + +if you do not enjoy me using your assets, i can remove it from this project and replace them with other things ## contributors @@ -35,12 +39,10 @@ if you do not enjoy me using your midi or songs, i can remove it from this proje ### generic -- redo scoring because it currently sucks -- visual aid for timing -- life bar +- prevent spamming during scoring +- visual aid for timing of square bounce (ghost square where next bounce?) - see entire map at the end - implement modifiers like osu (DT, HR, HD, EZ, etc) -- midi text events at specific timestamps to change the speed of the cube - more songs - map editor if time - rectangle duplicate overlap area remover for hallways @@ -48,9 +50,10 @@ if you do not enjoy me using your midi or songs, i can remove it from this proje - reset to default button for config page - square bounce intensity, particle intensity (sorry, TheCodingCrafter! merge conflicts strike again) - i think there are sync issues idk +- keystrokes icons for space, arrow keys before reactivating arrow keys and space +- 4 key gamemode ### modifiers - light switch that turns on and off at each bounce ? (flashlight) - precise mode (no 100s, only 300s or misses) -- theatre mode (no scoring) diff --git a/assets/basehiticon.png b/assets/basehiticon.png new file mode 100644 index 0000000000000000000000000000000000000000..9006ef2dad7b857a4c18f3b4fb97add89dd07cb5 GIT binary patch literal 152 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

nl7CjUy2;tvx2w$+RF|LdB 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 literal 0 HcmV?d00001 diff --git a/assets/late.png b/assets/late.png new file mode 100644 index 0000000000000000000000000000000000000000..a7a0da22007262c02e7ef2a1e87a0b08ffe8f463 GIT binary patch literal 1499 zcmV<11tj{3P)002t}1^@s6I8J)%00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1#n43K~#8N?VEpy z6;%|+&wblZwp}eNDyXQ4%&5dvlA^L-LiCHM6_G_KDyT3h!UCf|3`|PupRj)-Ng+%8 zPXeVLq!kscl*MW!B^0}{q3(OzTCDBfzUSTZeDh}J{k}8Lf%Ad0_netCciuU3f6b32 zMNt$*Q4~c{6h%=KrP~-OyN)f;V*Hvrpw5L{L>6M`4CtprxSSUG4{{tb2`NBE;Wq;R z-%_p(3_>r>n_Af)-JZRwB@qFwb{eJH*mIi0WC>Pant@Z*(PDNYd3P*5T~S z-IheykA&SQx))MUmMzlbDBgEg;v7W6t`)swa#E%OTGaC>(n=IF7Z>M2(K{x!mW4zO z;%py^S&t!%JBQ%=2lOM*Cr}nqPEijAcZ)&ZlrC})^m9<22jOo06eP+FtuV7`n^>nl z_BdUND|{4UW2Hdb>;ye($4bGs437UKw%#KBY;9>K%?=3WqnJQg4q)HS;?@>8esxT} zEO|Rg7mHgqKrqW<0%199$LUIOYYWtIOQ_DWjM%@ZPu#Kte)t!mK+4k5z{|-%9mAnI zouEsLTXw(?zdsU)3uddhv`x)%YJ%Pomu!I_zAHAMzv+5$X$#cxUZhSZd&MPNU_4qF z6AaIMAB#)N*gvVaW{WCP0T;*H0yK`~7Nv%s5UrXVZ4#{|kcj1UHVvcR$7EAAIG^dG z17c+hTE6?(c>~0G9FF}&lqWmdPM^p;*oE(xpxD~_(Rir1yr6%aU_rj0Iq>;dJ z_2gwaG@BrR9if0&CK|1#;gb&?Q(Fo&iHj27B@@E?n1vwtUhNTQ1+cWm7~J30014n$ zx71k;u(TcbeAi)&+AdpC!T$hnYAiN$4R2-_OIeSdkcZ7I8ZhV5VYey6poPSB!G^OG zTI78($Cc$*>|Z3VUBhe3!kBaxifGH?Zqop?mQUgLu9#u2K#Sdo`g_FAK0M&Yw2GEi z?5JZPwhkvd#4YvDS z2)~)y;M8>R*$t!Pm8DV9Is$93Z&7S?RuvEN%qe=}_-t9dq6Ha~vUB=MW7IFs2w>S*|MU^a&L^A0xS1<2zn5M_6C%zAn^pw4|_R>;*@VtB^el}C_op?J|*f~8Fe@ENhHu*c|f-09tgUvMY<_|eQIZcl`R zkgwpg2jk)%9K&DUyiwXyVF*i7BhHk|iaOiG=J91+pKq#>Y2PXuV5O*NH&gWf>NiMDX4XgYd!6d7-lBCo^r)fmYGDnfDIih>V?`)VNrk6(1vV!j-`M)o+ z{VG3cFi!F3+x%_4q9}@@D2k#eilQirq9`XJ`X9*^s>v$`A94Ty002ovPDHLkV1h6~ B$C3a5 literal 0 HcmV?d00001 diff --git a/assets/miss.png b/assets/miss.png new file mode 100644 index 0000000000000000000000000000000000000000..4323037059bf40f870ba6d1be8e529d30b97c7e3 GIT binary patch literal 1736 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 literal 0 HcmV?d00001 diff --git a/config.py b/config.py index 8035be7..df1b6a9 100644 --- a/config.py +++ b/config.py @@ -13,6 +13,12 @@ class Config: PARTICLE_SPEED = 10 # colors + # + # each color theme requires a hallway color, a background color, and at least one square color + # optionally, the color theme provides an hp_bar_border color (default 10, 9, 8), + # an hp_bar_background color (default 34, 51, 59), and a list + # of hp_bar_fill colors (default (156, 198, 155), (189, 228, 168), (215, 242, 186)) + # color_themes = { "dark": { "hallway": pygame.Color(214, 209, 205), @@ -105,6 +111,8 @@ class Config: volume: Optional[int] = 70 music_offset: Optional[int] = -300 direction_change_chance: Optional[int] = 30 + hp_drain_rate = 10 + theatre_mode = False # settings that are not configurable (yet) backtrack_chance: Optional[float] = 0.01 @@ -119,7 +127,7 @@ class Config: # keys to save and load save_attrs = ["theme", "seed", "camera_mode", "start_playing_delay", "max_notes", "bounce_min_spacing", - "square_speed", "volume", "music_offset", "direction_change_chance"] + "square_speed", "volume", "music_offset", "direction_change_chance", "hp_drain_rate", "theatre_mode"] def get_colors(): diff --git a/configpage.py b/configpage.py index 9c54b32..a3e13e7 100644 --- a/configpage.py +++ b/configpage.py @@ -121,6 +121,18 @@ def __init__(self): manager=self.ui_manager ) + self.s_hp_drain_rate = pgui.elements.UIHorizontalSlider( + relative_rect=pygame.Rect((300, 510, 300, 30)), + start_value=Config.hp_drain_rate, + value_range=(3, 20), + manager=self.ui_manager + ) + self.s_hp_drain_rate_label = pgui.elements.UILabel( + relative_rect=pygame.Rect((30, 510, 240, 30)), + text=f"HP drain rate ({Config.hp_drain_rate}/s):", + manager=self.ui_manager + ) + # audio and general settings self.s_game_volume = pgui.elements.UIHorizontalSlider( @@ -147,6 +159,18 @@ def __init__(self): manager=self.ui_manager ) + self.s_theatre_mode = pgui.elements.UIDropDownMenu( + ["Off", "On"], + relative_rect=pygame.Rect((930, 150, 300, 30)), + starting_option=["Off", "On"][int(Config.theatre_mode)], + manager=self.ui_manager + ) + self.s_theatre_mode_label = pgui.elements.UILabel( + relative_rect=pygame.Rect((630, 150, 240, 30)), + text="Theatre mode:", + manager=self.ui_manager + ) + def handle_event(self, event: pygame.event.Event): if not self.active: return @@ -168,6 +192,9 @@ def handle_event(self, event: pygame.event.Event): if event.ui_element == self.s_color_theme: play_sound("wood.wav") Config.theme = event.text + if event.ui_element == self.s_theatre_mode: + play_sound("wood.wav") + Config.theatre_mode = bool("fn".index(event.text[1])) if event.type == pgui.UI_TEXT_ENTRY_CHANGED: if event.ui_element == self.s_seed: @@ -203,6 +230,9 @@ def handle_event(self, event: pygame.event.Event): if event.ui_element == self.s_direction_change_chance: self.s_direction_change_chance_label.set_text(f"Change dir chance ({event.value}%):") Config.direction_change_chance = event.value + if event.ui_element == self.s_hp_drain_rate: + self.s_hp_drain_rate_label.set_text(f"HP drain rate ({event.value}/s):") + Config.hp_drain_rate = event.value self.ui_manager.process_events(event) diff --git a/game.py b/game.py index fd65392..f42610b 100644 --- a/game.py +++ b/game.py @@ -3,6 +3,7 @@ from world import World import random from camera import Camera +from keystrokes import Keystrokes from particle import Particle @@ -17,6 +18,8 @@ def __init__(self): self.music_has_played = False self.offset_happened = False self.loading_text = get_font("./assets/poppins-regular.ttf", 24).render("Loading...", True, (255, 255, 255)) + self.try_again_text = get_font("./assets/poppins-regular.ttf", 36).render("Press escape to go back", True, (255, 255, 255)) + self.keystrokes = Keystrokes() def start_song(self, screen: pygame.Surface, song_path: str = None): random.seed(Config.seed) @@ -62,7 +65,6 @@ def update_loading_screen(pdone: int): self.world.square.pos = self.world.future_bounces[0].square_pos def draw(self, screen: pygame.Surface): - self.world.scorekeeper.penalize_misses(self.world.time) if not self.active: return @@ -129,24 +131,50 @@ def draw(self, screen: pygame.Surface): self.world.particles.remove(remove_particle) # scorekeeper drawing - mimic = get_font("./assets/poppins-regular.ttf", 24).render(self.world.scorekeeper.latest_message, True, (255, 255, 255)) - screen.blit(mimic, (100, 100)) + time_from_start = self.world.time-Config.start_playing_delay/1000+Config.music_offset/1000 + if not Config.theatre_mode: + self.world.scorekeeper.draw(screen, time_from_start if len(self.world.future_bounces) else -1) + + # hit icons + to_remove = [] + for hiticon in self.world.scorekeeper.hit_icons: + if hiticon.draw(screen, self.camera): + to_remove.append(hiticon) + for remove in to_remove: + self.world.scorekeeper.hit_icons.remove(remove) + + if self.world.scorekeeper.hp <= 0 and not self.world.square.died: + self.world.square.died = True + self.world.square.dir = [0, 0] + pygame.mixer.music.stop() + play_sound("death.mp3", 0.5) + self.world.future_bounces = [] + for _ in range(100): + self.world.particles.append(Particle(self.world.square.pos, [random.randint(-3, 3), random.randint(-3, 3)])) + if self.world.square.died: + self.world.scorekeeper.hp = 0 # draw square self.world.square.draw(screen, sqrect) - # countdown to start - time_from_start = self.world.time-Config.start_playing_delay/1000+Config.music_offset/1000 - if time_from_start < 0: - repr_time = f"{abs(int((time_from_start+0.065)*10)/10)}s" - countdown_surface = get_font("./assets/poppins-regular.ttf", 36).render(repr_time, True, (255, 255, 255)) - screen.blit(countdown_surface, countdown_surface.get_rect(center=(Config.SCREEN_WIDTH/2, Config.SCREEN_HEIGHT/4))) - elif time_from_start < 0.5: - repr_zero = f"0.0s" - countdown_surface = get_font("./assets/poppins-regular.ttf", 36).render(repr_zero, True, (255, 255, 255)) - countdown_surface.set_alpha((0.5-time_from_start)*2*255) - screen.blit(countdown_surface, countdown_surface.get_rect(center=(Config.SCREEN_WIDTH/2, Config.SCREEN_HEIGHT/4))) - + if not Config.theatre_mode: + # keystrokes + self.keystrokes.draw(screen) + + # countdown to start + if time_from_start < 0: + repr_time = f"{abs(int((time_from_start+0.065)*10)/10)}s" + countdown_surface = get_font("./assets/poppins-regular.ttf", 36).render(repr_time, True, (255, 255, 255)) + screen.blit(countdown_surface, countdown_surface.get_rect(center=(Config.SCREEN_WIDTH/2, Config.SCREEN_HEIGHT/4))) + elif time_from_start < 0.5: + repr_zero = f"0.0s" + countdown_surface = get_font("./assets/poppins-regular.ttf", 36).render(repr_zero, True, (255, 255, 255)) + countdown_surface.set_alpha((0.5-time_from_start)*2*255) + screen.blit(countdown_surface, countdown_surface.get_rect(center=(Config.SCREEN_WIDTH/2, Config.SCREEN_HEIGHT/4))) + + # failure message + if self.world.square.died: + screen.blit(self.try_again_text, self.try_again_text.get_rect(center=(Config.SCREEN_WIDTH/2, Config.SCREEN_HEIGHT/4))) if not self.camera.locked_on_square: screen.blit(self.camera_ctrl_text, (10, 10)) @@ -162,10 +190,12 @@ def handle_event(self, event: pygame.event.Event): return True if event.key == pygame.K_TAB: self.camera.locked_on_square = not self.camera.locked_on_square - if self.camera.locked_on_square: - time_from_start = self.world.time-Config.start_playing_delay/1000+Config.music_offset/1000 - if time_from_start < 0: - return - arrows_n_space = pygame.K_SPACE, pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN - if 97+26 > event.key >= 97 or event.key in arrows_n_space: # press a to z key or space or arrows - self.world.handle_keypress() + if not Config.theatre_mode: + if self.camera.locked_on_square: + time_from_start = self.world.time-Config.start_playing_delay/1000+Config.music_offset/1000 + if time_from_start < -0.2: + return + # arrows_n_space = pygame.K_SPACE, pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN + arrows_n_space = () + if 97+26 > event.key >= 97 or event.key in arrows_n_space: # press a to z key or space or arrows + self.world.handle_keypress(time_from_start) diff --git a/hiticon.py b/hiticon.py new file mode 100644 index 0000000..c2e485b --- /dev/null +++ b/hiticon.py @@ -0,0 +1,30 @@ +from utils import * +import pygame + + +class HitLevel(Enum): + early = 0 + good = 1 + perfect = 2 + late = 3 + miss = 4 + + +class HitIcon: + surfaces: dict[HitLevel, pygame.Surface] = {} + + def __init__(self, lvl: HitLevel, pos: Union[tuple[int, int], list[int]]): + if len(HitIcon.surfaces) == 0: + for level in HitLevel: + HitIcon.surfaces[level] = pygame.image.load(f"./assets/{level.name}.png").convert_alpha() + + self.pos = pos + self.lvl = lvl + self.age_left = 500 + self.surf = HitIcon.surfaces[lvl].copy() + + def draw(self, screen: pygame.Surface, camera): + self.age_left -= 500/FRAMERATE + self.surf.set_alpha(int(max(min(self.age_left, 255), 0))) + screen.blit(self.surf, camera.offset(self.surf.get_rect(center=self.pos))) + return self.age_left <= 0 diff --git a/keystrokes.py b/keystrokes.py new file mode 100644 index 0000000..ec0058a --- /dev/null +++ b/keystrokes.py @@ -0,0 +1,29 @@ +from utils import * +import pygame + + +class Keystrokes: + def __init__(self): + self.keys: list[str] = [] + self.key_surfaces: dict[str, pygame.Surface] = {} + self.chars = "abcdefghijklmnopqrstuvwxyz" + for possible_to_hit in self.chars: + self.key_surfaces[possible_to_hit] = get_font("./assets/poppins-regular.ttf", 36).render( + possible_to_hit, True, get_colors()["background"] + ) + + def draw(self, screen: pygame.Surface): + default_rect = pygame.Rect(10, Config.SCREEN_HEIGHT-60, 50, 50) + for _ in range(97, 97+26): + char = self.chars[_-97] + if pygame.key.get_pressed()[_]: + if char not in self.keys: + self.keys.append(char) + else: + if char in self.keys: + self.keys.remove(char) + for key in self.keys: + pygame.draw.rect(screen, (0, 0, 0), default_rect) + pygame.draw.rect(screen, get_colors()["hallway"], default_rect.inflate(-2, -2)) + screen.blit(self.key_surfaces[key], self.key_surfaces[key].get_rect(center=default_rect.center)) + default_rect.x += 60 diff --git a/scorekeeper.py b/scorekeeper.py index 50f4a60..71ca1f6 100644 --- a/scorekeeper.py +++ b/scorekeeper.py @@ -1,47 +1,72 @@ from utils import * import pygame +from hiticon import HitIcon, HitLevel class Scorekeeper: - def __init__(self): + def __init__(self, world): + self.world = world self.unhit_notes: list[float] = [] - self.score = 0 - self.latest_message: Optional[str] = None + self.hit_icons: list[HitIcon] = [] + self.hp = 100 + self.shown_hp = 0 - def penalize_misses(self, current_time: float): - new = [] - for note in self.unhit_notes: - t = current_time - note + Config.music_offset / 1000 - Config.start_playing_delay / 1000 - if t > 0.1: - self.score -= 100 - self.latest_message = f"Score: {self.score}" - continue - new.append(note) - self.unhit_notes = new + @property + def life_bar_rect(self): + return pygame.Rect((10, 10, int(Config.SCREEN_WIDTH/2-30), 30)) - def attempt_log(self, current_time: float): + def draw(self, screen: pygame.Surface, current_time: float): + self.hp = max(0, min(self.hp, 100)) + bg_color = get_colors().get("hp_bar_background", pygame.Color(34, 51, 59)) + border_color = get_colors().get("hp_bar_border", pygame.Color(10, 9, 8)) + fill_colors = get_colors().get("hp_bar_fill", ( + pygame.Color(156, 198, 155), pygame.Color(189, 228, 168), pygame.Color(215, 242, 186) + )) + screen.fill(border_color, self.life_bar_rect) + inner = self.life_bar_rect.inflate(-8, -8) + screen.fill(bg_color, inner) + bar_width = inner.width / len(fill_colors) + bar_hp_repr = 100 / len(fill_colors) + leftover = self.shown_hp + for _ in range(len(fill_colors)): + chunk_rect = inner.copy() + chunk_rect.x = inner.x + bar_width * _ + width = min(max(leftover, 0), int(bar_hp_repr)+1)/bar_hp_repr*bar_width + leftover -= bar_hp_repr + chunk_rect.width = width + screen.fill(fill_colors[_], chunk_rect) + if current_time > 0: + self.hp -= Config.hp_drain_rate/FRAMERATE + hp_show_damping = 10 + self.shown_hp = self.shown_hp*(1-hp_show_damping/FRAMERATE)+self.hp*(hp_show_damping/FRAMERATE) + + # remove unhit notes + to_remove = [] + for timestamp in self.unhit_notes: + if timestamp+0.12 < current_time: # 120ms late is missed note + self.hp -= 6 + self.hit_icons.append(HitIcon(HitLevel.miss, self.world.square.pos)) + to_remove.append(timestamp) + for t_remove in to_remove: + self.unhit_notes.remove(t_remove) + + def do_keypress(self, current_time: float): # negative closest means hit before, positive means hit after - abs_closest = 100 - closest = 100 - to_remove = None - for unhit in self.unhit_notes: - t = current_time - unhit + Config.music_offset / 1000 - Config.start_playing_delay / 1000 - if abs(t) < abs_closest: - abs_closest = int(abs(t)*100)/100 - closest = int(t*100)/100 - to_remove = unhit - if closest > 0.1 or closest < -0.08: - self.score -= 300 - self.latest_message = f"Score: {self.score}" + for timestamp in self.unhit_notes: + offset = current_time-timestamp + if offset > 0.06: # 60ms late - 120ms late + self.hit_icons.append(HitIcon(HitLevel.late, self.world.square.pos.copy())) + self.hp -= 1 + elif offset > -0.06: # 60ms early - 60ms late + self.hit_icons.append(HitIcon(HitLevel.perfect, self.world.square.pos.copy())) + self.hp += 3 + elif offset > -0.09: # 60ms early - 90ms early + self.hit_icons.append(HitIcon(HitLevel.good, self.world.square.pos.copy())) + self.hp -= 1 + elif offset > -0.12: # 90ms early - 120ms early + self.hit_icons.append(HitIcon(HitLevel.early, self.world.square.pos.copy())) + self.hp -= 2 + else: + return + self.unhit_notes.remove(timestamp) return - if to_remove is not None: - self.unhit_notes.remove(to_remove) - if closest > 0.04: - self.score += 100 - elif closest > -0.02: - self.score += 300 - elif closest > -0.04: - self.score += 100 - else: - self.score -= 100 - self.latest_message = f"Score: {self.score}" diff --git a/square.py b/square.py index 85b798d..12b517f 100644 --- a/square.py +++ b/square.py @@ -10,6 +10,7 @@ def __init__(self, x: float = 0, y: float = 0, dx: int = 1, dy: int = 1): self.last_bounce_time = -100 self.latest_bounce_direction = 0 # 0 = horiz, 1 = vert self.past_colors = [] + self.died = False def register_past_color(self, col: tuple[int, int, int]): for _ in range(max(Config.square_swipe_anim_speed, 1)): @@ -60,6 +61,8 @@ def title_screen_physics(self, bounding: pygame.Rect): return True def draw(self, screen: pygame.Surface, sqrect: pygame.Rect): + if self.died: + return pygame.draw.rect(screen, (0, 0, 0), sqrect) 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"])]) diff --git a/world.py b/world.py index ac1e6ef..81d1f44 100644 --- a/world.py +++ b/world.py @@ -20,12 +20,12 @@ def __init__(self): self.particles: list[Particle] = [] self.timestamps = [] self.square = Square() - self.scorekeeper = Scorekeeper() + self.scorekeeper = Scorekeeper(self) def update_time(self) -> None: self.time = get_current_time() - self.start_time - def next_bounce(self): + def next_bounce(self) -> Bounce: self.past_bounces.append(self.future_bounces.pop(0)) return self.past_bounces[-1] @@ -53,8 +53,8 @@ def handle_bouncing(self, square: Square): square.dir = [0, 0] square.pos = current_bounce.square_pos - def handle_keypress(self): - self.scorekeeper.attempt_log(self.time) + def handle_keypress(self, time_from_start): + self.scorekeeper.do_keypress(time_from_start) def gen_future_bounces(self, _start_notes: list[tuple[int, int, int]], percent_update_callback): """Recursive solution is necessary""" @@ -149,7 +149,7 @@ def recurs( _start_notes = _start_notes[:Config.max_notes] if Config.max_notes is not None else _start_notes - self.scorekeeper.unhit_notes = remove_too_close_values([_sn[1] for _sn in _start_notes[:-1]], Config.bounce_min_spacing) + self.scorekeeper.unhit_notes = remove_too_close_values([_sn[1] for _sn in _start_notes], Config.bounce_min_spacing) self.future_bounces = recurs( square=self.square.copy(),