add modifiers

This commit is contained in:
Yonokid
2025-08-26 23:46:16 -04:00
parent dc5a46fec2
commit 3de1dbc79f
7 changed files with 246 additions and 40 deletions

View File

@@ -2,7 +2,6 @@
fps_counter = false
judge_offset = 0
visual_offset = 0
autoplay = false
language = "ja"
hard_judge = 108

View File

@@ -1,12 +1,13 @@
import bisect
import hashlib
import math
import random
from collections import deque
from dataclasses import dataclass, field, fields
from functools import lru_cache
from pathlib import Path
from libs.utils import get_pixels_per_frame, strip_comments
from libs.utils import get_pixels_per_frame, global_data, strip_comments
@lru_cache(maxsize=64)
@@ -584,6 +585,7 @@ class TJAParser:
continue
note = Note()
note.hit_ms = self.current_ms
note.display = True
note.pixels_per_frame_x = bar_line.pixels_per_frame_x
note.pixels_per_frame_y = bar_line.pixels_per_frame_y
pixels_per_ms = get_pixels_per_ms(note.pixels_per_frame_x)
@@ -628,6 +630,7 @@ class TJAParser:
# Sorting by load_ms is necessary for drawing, as some notes appear on the
# screen slower regardless of when they reach the judge circle
# Bars can be sorted like this because they don't need hit detection
print(play_note_list[0])
return deque(play_note_list), deque(draw_note_list), deque(bar_list)
def hash_note_data(self, play_notes: deque[Note | Drumroll | Balloon], bars: deque[Note]):
@@ -650,3 +653,47 @@ class TJAParser:
n.update(item.get_hash().encode('utf-8'))
return n.hexdigest()
def modifier_speed(notes: deque[Note | Balloon | Drumroll], bars, value: float):
notes = notes.copy()
for note in notes:
note.pixels_per_frame_x *= value
note.load_ms = note.hit_ms - (866 / get_pixels_per_ms(note.pixels_per_frame_x))
for bar in bars:
bar.pixels_per_frame_x *= value
bar.load_ms = bar.hit_ms - (866 / get_pixels_per_ms(bar.pixels_per_frame_x))
return notes, bars
def modifier_display(notes: deque[Note | Balloon | Drumroll]):
notes = notes.copy()
for note in notes:
note.display = False
return notes
def modifier_inverse(notes: deque[Note | Balloon | Drumroll]):
notes = notes.copy()
type_mapping = {1: 2, 2: 1, 3: 4, 4: 3}
for note in notes:
if note.type in type_mapping:
note.type = type_mapping[note.type]
return notes
def modifier_random(notes: deque[Note | Balloon | Drumroll], value: int):
#value: 1 == kimagure, 2 == detarame
notes = notes.copy()
percentage = int(len(notes) / 5) * value
selected_notes = random.sample(range(len(notes)), percentage)
type_mapping = {1: 2, 2: 1, 3: 4, 4: 3}
for i in selected_notes:
if notes[i].type in type_mapping:
notes[i].type = type_mapping[notes[i].type]
return notes
def apply_modifiers(notes: deque[Note | Balloon | Drumroll], draw_notes: deque[Note | Balloon | Drumroll], bars: deque[Note]):
if global_data.modifiers.display:
draw_notes = modifier_display(draw_notes)
if global_data.modifiers.inverse:
notes = modifier_inverse(notes)
notes = modifier_random(notes, global_data.modifiers.random)
draw_notes, bars = modifier_speed(draw_notes, bars, global_data.modifiers.speed)
return notes, draw_notes, bars

View File

@@ -197,6 +197,14 @@ def is_r_kat_pressed() -> bool:
return False
@dataclass
class Modifiers:
auto: bool = False
speed: float = 1.0
display: bool = False
inverse: bool = False
random: int = 0
@dataclass
class SessionData:
selected_difficulty: int = 0
@@ -227,6 +235,7 @@ class GlobalData:
total_songs: int = 0
hit_sound: int = 0
player_num: int = 1
modifiers: Modifiers = field(default_factory=lambda: Modifiers())
global_data = GlobalData()

View File

@@ -10,29 +10,23 @@ class DevScreen:
self.width = width
self.height = height
self.screen_init = False
tex.load_screen_textures('song_select')
self.history = ScoreHistory({0: (583892, 0, 0, 0),
1: (234941, 0, 0, 0),
2: (867847, 0, 0, 0),
3: (485589, 0, 0, 0),
4: (1584395, 0, 0, 0)}, get_current_ms())
def on_screen_start(self):
if not self.screen_init:
self.screen_init = True
def on_screen_end(self, next_screen: str):
self.screen_init = False
return next_screen
def update(self):
self.on_screen_start()
self.history.update(get_current_ms())
if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
return self.on_screen_end('RESULT')
return self.on_screen_end('GAME')
def draw(self):
self.history.draw()
pass
def draw_3d(self):
pass

View File

@@ -11,7 +11,14 @@ from libs.animation import Animation
from libs.audio import audio
from libs.backgrounds import Background
from libs.texture import tex
from libs.tja import Balloon, Drumroll, Note, TJAParser, calculate_base_score
from libs.tja import (
Balloon,
Drumroll,
Note,
TJAParser,
apply_modifiers,
calculate_base_score,
)
from libs.transition import Transition
from libs.utils import (
OutlinedText,
@@ -34,6 +41,7 @@ class GameScreen:
self.current_ms = 0
self.screen_init = False
self.movie = None
self.song_music = None
self.end_ms = 0
self.start_delay = 1000
self.song_started = False
@@ -66,15 +74,13 @@ class GameScreen:
else:
self.movie = None
session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
if not hasattr(self, 'song_music'):
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file():
self.song_music = audio.load_music_stream(self.tja.metadata.wave)
audio.normalize_music_stream(self.song_music, 0.1935)
else:
self.song_music = None
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
self.player_1 = Player(self, global_data.player_num, difficulty)
if self.tja is not None:
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
def on_screen_start(self):
if not self.screen_init:
@@ -107,7 +113,7 @@ class GameScreen:
def write_score(self):
if self.tja is None:
return
if global_data.config['general']['autoplay']:
if global_data.modifiers.auto:
return
with sqlite3.connect('scores.db') as con:
cursor = con.cursor()
@@ -210,6 +216,7 @@ class Player:
if game_screen.tja is not None:
self.play_notes, self.draw_note_list, self.draw_bar_list = game_screen.tja.notes_to_position(self.difficulty)
self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(self.play_notes, self.draw_note_list, self.draw_bar_list)
else:
self.play_notes, self.draw_note_list, self.draw_bar_list = deque(), deque(), deque()
self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5])
@@ -502,7 +509,7 @@ class Player:
self.input_log[game_screen.current_ms] = (note_type, side)
def autoplay_manager(self, game_screen: GameScreen):
if not global_data.config["general"]["autoplay"]:
if not global_data.modifiers.auto:
return
if len(self.play_notes) == 0:
return
@@ -582,6 +589,7 @@ class Player:
end_position = self.get_position_x(game_screen.width, game_screen.current_ms, tail.load_ms, tail.pixels_per_frame_x)
length = end_position - start_position
color = ray.Color(255, head.color, head.color, 255)
if head.display:
tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192, x2=length-64-32, color=color)
if is_big:
tex.draw_texture('notes', "drumroll_big_tail", x=end_position, y=192, color=color)
@@ -605,6 +613,7 @@ class Player:
position = pause_position
else:
position = start_position
if head.display:
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=192)
tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+128, y=192)
@@ -650,9 +659,27 @@ class Player:
self.draw_balloon(game_screen, note, current_eighth)
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position)
else:
if note.display:
tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192, center=True)
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position)
def draw_modifiers(self):
tex.draw_texture('lane', 'mod_shinuchi')
if global_data.modifiers.speed >= 4:
tex.draw_texture('lane', 'mod_yonbai')
elif global_data.modifiers.speed >= 3:
tex.draw_texture('lane', 'mod_sanbai')
elif global_data.modifiers.speed > 1:
tex.draw_texture('lane', 'mod_baisaku')
if global_data.modifiers.display:
tex.draw_texture('lane', 'mod_doron')
if global_data.modifiers.inverse:
tex.draw_texture('lane', 'mod_abekobe')
if global_data.modifiers.random == 2:
tex.draw_texture('lane', 'mod_detarame')
elif global_data.modifiers.random == 1:
tex.draw_texture('lane', 'mod_kimagure')
def draw(self, game_screen: GameScreen):
tex.draw_texture('lane', 'lane_background')
self.gauge.draw()
@@ -665,7 +692,7 @@ class Player:
self.draw_notes(game_screen)
tex.draw_texture('lane', f'{self.player_number}p_lane_cover')
tex.draw_texture('lane', 'drum')
if global_data.config["general"]["autoplay"]:
if global_data.modifiers.auto:
tex.draw_texture('lane', 'auto_icon')
for anim in self.draw_drum_hit_list:
anim.draw()
@@ -673,6 +700,7 @@ class Player:
tex.draw_texture('lane', 'lane_score_cover')
tex.draw_texture('lane', f'{self.player_number}p_icon')
tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty)
self.draw_modifiers()
if self.drumroll_counter is not None:
self.drumroll_counter.draw()
for anim in self.draw_arc_list:

View File

@@ -391,7 +391,6 @@ class ScoreAnimator:
if int(ret_val) == 0:
if not (len(self.target_score) - self.digit_index) > (len(self.target_score)):
return '0' * (len(self.target_score) - self.digit_index)
else:
return '0'
return str(int(ret_val))

View File

@@ -1,3 +1,4 @@
from dataclasses import fields
import random
import sqlite3
from datetime import datetime, timedelta
@@ -12,6 +13,7 @@ from libs.texture import tex
from libs.tja import TJAParser, test_encodings
from libs.transition import Transition
from libs.utils import (
Modifiers,
OutlinedText,
get_current_ms,
global_data,
@@ -72,6 +74,7 @@ class SongSelectScreen:
self.demo_song = None
self.diff_sort_selector = None
self.neiro_selector = None
self.modifier_selector = None
self.texture_index = 9
self.last_texture_index = 9
self.last_moved = get_current_ms()
@@ -169,7 +172,6 @@ class SongSelectScreen:
def handle_input_selected(self):
# Handle song selection confirmation or cancel
if self.neiro_selector is not None:
if is_l_kat_pressed() or is_r_kat_pressed():
if is_l_kat_pressed():
self.neiro_selector.move_left()
elif is_r_kat_pressed():
@@ -178,11 +180,23 @@ class SongSelectScreen:
audio.play_sound(self.sound_don)
self.neiro_selector.confirm()
return
if self.modifier_selector is not None:
if is_l_kat_pressed():
audio.play_sound(self.sound_kat)
self.modifier_selector.left()
elif is_r_kat_pressed():
audio.play_sound(self.sound_kat)
self.modifier_selector.right()
if is_l_don_pressed() or is_r_don_pressed():
audio.play_sound(self.sound_don)
self.modifier_selector.confirm()
return
if is_l_don_pressed() or is_r_don_pressed():
if self.selected_difficulty == -3:
self._cancel_selection()
elif self.selected_difficulty == -2:
pass
audio.play_sound(self.sound_don)
self.modifier_selector = ModifierSelector()
elif self.selected_difficulty == -1:
audio.play_sound(self.sound_don)
self.neiro_selector = NeiroSelector()
@@ -378,6 +392,11 @@ class SongSelectScreen:
if self.neiro_selector.is_finished:
self.neiro_selector = None
if self.modifier_selector is not None:
self.modifier_selector.update(get_current_ms())
if self.modifier_selector.is_finished:
self.modifier_selector = None
for song in self.navigator.items:
song.box.update(self.state == State.SONG_SELECTED)
song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150
@@ -397,7 +416,7 @@ class SongSelectScreen:
return self.on_screen_end('ENTRY')
def draw_selector(self):
fade = 0.5 if self.neiro_selector is not None else 1.0
fade = 0.5 if self.neiro_selector is not None else self.text_fade_in.attribute
direction = 1 if self.diff_select_move_right else -1
if self.selected_difficulty <= -1 or self.prev_diff == -1:
if self.prev_diff == -1 and self.selected_difficulty >= 0:
@@ -464,6 +483,9 @@ class SongSelectScreen:
if self.neiro_selector is not None:
self.neiro_selector.draw()
if self.modifier_selector is not None:
self.modifier_selector.draw()
if self.game_transition is not None:
self.game_transition.draw()
@@ -839,7 +861,6 @@ class YellowBox:
return
tex.draw_texture('diff_select', 'back', fade=self.fade_in.attribute)
tex.draw_texture('diff_select', 'option', fade=self.fade_in.attribute)
tex.draw_texture('diff_select', 'disable', fade=min(0.5, self.fade_in.attribute))
tex.draw_texture('diff_select', 'neiro', fade=self.fade_in.attribute)
for i in range(4):
@@ -1216,6 +1237,115 @@ class NeiroSelector:
dest = ray.Rectangle((self.direction*-100) + 235 - (self.text_2.texture.width//2) + (self.move_sideways.attribute*self.direction), y+1000, self.text_2.texture.width, self.text_2.texture.height)
self.text_2.draw(self.text_2.default_src, dest, ray.Vector2(0, 0), 0, ray.fade(ray.WHITE, 1 - self.fade_sideways.attribute))
class ModifierSelector:
TEX_MAP = {
"auto": "mod_auto",
"speed": "mod_baisaku",
"display": "mod_doron",
"inverse": "mod_abekobe",
"random": "mod_kimagure"
}
NAME_MAP = {
"auto": "オート",
"speed": "はやさ",
"display": "ドロン",
"inverse": "あべこべ",
"random": "ランダム"
}
def __init__(self):
self.mods = fields(Modifiers)
self.current_mod_index = 0
self.is_confirmed = False
self.is_finished = False
self.move = tex.get_animation(28)
self.move.start()
self.text = [OutlinedText(ModifierSelector.NAME_MAP[mod.name], 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) for mod in self.mods]
self.text_true = OutlinedText('する', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5)
self.text_false = OutlinedText('じゃない', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5)
self.text_speed = OutlinedText(str(global_data.modifiers.speed), 30, ray.WHITE, ray.BLACK, outline_thickness=3.5)
def update(self, current_ms):
self.is_finished = self.is_confirmed and self.move.is_finished
if self.is_finished:
for text in self.text:
text.unload()
self.move.update(current_ms)
def confirm(self):
if self.is_confirmed:
return
self.current_mod_index += 1
if self.current_mod_index == len(self.mods):
self.is_confirmed = True
self.move.restart()
def left(self):
if self.is_confirmed:
return
current_mod = self.mods[self.current_mod_index]
current_value = getattr(global_data.modifiers, current_mod.name)
if current_mod.type is bool:
setattr(global_data.modifiers, current_mod.name, not current_value)
elif current_mod.name == 'speed':
setattr(global_data.modifiers, current_mod.name, max(0.1, current_value-0.1))
self.text_speed.unload()
self.text_speed = OutlinedText(str(global_data.modifiers.speed), 30, ray.WHITE, ray.BLACK, outline_thickness=3.5)
elif current_mod.name == 'random':
setattr(global_data.modifiers, current_mod.name, max(0, current_value-1))
def right(self):
if self.is_confirmed:
return
current_mod = self.mods[self.current_mod_index]
current_value = getattr(global_data.modifiers, current_mod.name)
if current_mod.type is bool:
setattr(global_data.modifiers, current_mod.name, not current_value)
elif current_mod.name == 'speed':
setattr(global_data.modifiers, current_mod.name, current_value+0.1)
self.text_speed.unload()
self.text_speed = OutlinedText(str(global_data.modifiers.speed), 30, ray.WHITE, ray.BLACK, outline_thickness=3.5)
elif current_mod.name == 'random':
setattr(global_data.modifiers, current_mod.name, min(2, current_value+1))
def draw(self):
if self.is_confirmed:
move = self.move.attribute - 370
else:
move = -self.move.attribute
tex.draw_texture('modifier', 'top', y=move)
tex.draw_texture('modifier', f'{global_data.player_num}p', y=move)
tex.draw_texture('modifier', 'bottom', y=move + (len(self.mods)*50))
for i in range(len(self.mods)):
tex.draw_texture('modifier', 'background', y=move + (i*50))
if i == self.current_mod_index:
tex.draw_texture('modifier', 'mod_bg_highlight', y=move + (i*50))
else:
tex.draw_texture('modifier', 'mod_bg', y=move + (i*50))
tex.draw_texture('modifier', 'mod_box', y=move + (i*50))
dest = ray.Rectangle(92, 819 + move + (i*50), self.text[i].texture.width, self.text[i].texture.height)
self.text[i].draw(self.text[i].default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
current_mod = self.mods[i]
current_value = getattr(global_data.modifiers, current_mod.name)
if current_mod.type is bool:
if current_value:
tex.draw_texture('modifier', ModifierSelector.TEX_MAP[self.mods[i].name], y=move + (i*50))
dest = ray.Rectangle(330 - (self.text_true.texture.width//2), 819 + move + (i*50), self.text_true.texture.width, self.text_true.texture.height)
self.text_true.draw(self.text_true.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
else:
dest = ray.Rectangle(330 - (self.text_false.texture.width//2), 819 + move + (i*50), self.text_false.texture.width, self.text_false.texture.height)
self.text_false.draw(self.text_false.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
elif current_mod.name == 'speed':
dest = ray.Rectangle(330 - (self.text_speed.texture.width//2), 819 + move + (i*50), self.text_speed.texture.width, self.text_speed.texture.height)
self.text_speed.draw(self.text_speed.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE)add
if current_value > 1.0:
tex.draw_texture('modifier', ModifierSelector.TEX_MAP[self.mods[i].name], y=move + (i*50))
elif current_value >= 3.0:
tex.draw_texture('modifier', 'mod_sanbai', y=move + (i*50))
elif current_value >= 4.0:
tex.draw_texture('modifier', 'mod_yonbai', y=move + (i*50))
elif current_mod.name == 'random':
if current_value == 1:
tex.draw_texture('modifier', ModifierSelector.TEX_MAP[self.mods[i].name], y=move + (i*50))
elif current_value == 2:
tex.draw_texture('modifier', 'mod_detarame', y=move + (i*50))
class ScoreHistory:
def __init__(self, scores: dict[int, tuple[int, int, int, int]], current_ms):
self.scores = {k: v for k, v in scores.items() if v is not None}