Files
PyTaiko/libs/utils.py
2025-05-12 15:33:25 -04:00

376 lines
14 KiB
Python

import os
import tempfile
import time
import tomllib
import zipfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import pyray as ray
#TJA Format creator is unknown. I did not create the format, but I did write the parser though.
def get_zip_filenames(zip_path: Path) -> list[str]:
result = []
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
file_list = zip_ref.namelist()
for file_name in file_list:
result.append(file_name)
return result
def load_image_from_zip(zip_path: Path, filename: str) -> ray.Image:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
with zip_ref.open(filename) as image_file:
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
temp_file.write(image_file.read())
temp_file_path = temp_file.name
image = ray.load_image(temp_file_path)
os.remove(temp_file_path)
return image
def load_texture_from_zip(zip_path: Path, filename: str) -> ray.Texture:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
with zip_ref.open(filename) as image_file:
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
temp_file.write(image_file.read())
temp_file_path = temp_file.name
texture = ray.load_texture(temp_file_path)
os.remove(temp_file_path)
return texture
def load_all_textures_from_zip(zip_path: Path) -> dict[str, list[ray.Texture]]:
result_dict = dict()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
files = zip_ref.namelist()
for file in files:
with zip_ref.open(file) as image_file:
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
temp_file.write(image_file.read())
temp_file_path = temp_file.name
texture = ray.load_texture(temp_file_path)
os.remove(temp_file_path)
true_filename, index = file.split('_img')
index = int(index.split('.')[0])
if true_filename not in result_dict:
result_dict[true_filename] = []
while len(result_dict[true_filename]) <= index:
result_dict[true_filename].append(None)
result_dict[true_filename][index] = texture
return result_dict
def rounded(num: float) -> int:
sign = 1 if (num >= 0) else -1
num = abs(num)
result = int(num)
if (num - result >= 0.5):
result += 1
return sign * result
def get_current_ms() -> int:
return rounded(time.time() * 1000)
def strip_comments(code: str) -> str:
result = ''
index = 0
for line in code.splitlines():
comment_index = line.find('//')
if comment_index == -1:
result += line
elif comment_index != 0 and not line[:comment_index].isspace():
result += line[:comment_index]
index += 1
return result
def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> float:
beat_duration = 60 / bpm
total_time = time_signature * beat_duration
total_frames = 60 * total_time
return (distance / total_frames)
def get_config() -> dict[str, Any]:
with open('config.toml', "rb") as f:
config_file = tomllib.load(f)
return config_file
def draw_scaled_texture(texture: ray.Texture, x: int, y: int, scale: float, color: ray.Color) -> None:
src_rect = ray.Rectangle(0, 0, texture.width, texture.height)
dst_rect = ray.Rectangle(x, y, texture.width*scale, texture.height*scale)
ray.draw_texture_pro(texture, src_rect, dst_rect, ray.Vector2(0, 0), 0, color)
@dataclass
class SessionData:
selected_song: str = '' #Path
selected_difficulty: int = 0
song_title: str = ''
result_score: int = 0
result_good: int = 0
result_ok: int = 0
result_bad: int = 0
result_max_combo: int = 0
result_total_drumroll: int = 0
result_gauge_length: int = 0
session_data = SessionData()
def reset_session():
return SessionData()
@dataclass
class GlobalData:
songs_played: int = 0
global_data = GlobalData()
@dataclass
class OutlinedText:
font: ray.Font
text: str
font_size: int
text_color: ray.Color
outline_color: ray.Color
outline_thickness: int = 2
vertical: bool = False
line_spacing: float = 1.0 # Line spacing for vertical text
lowercase_spacing_factor: float = 0.85 # Adjust spacing for lowercase letters and whitespace
vertical_chars: set = field(default_factory=lambda: {'-', '|', '/', '\\', ''})
no_space_chars: set = field(default_factory=lambda: {
'', '','', '','', '','', '','', '',
'', '','', '','', '','', '','', '',
'', '','','','','','','','','','',
'','','','','','',''
})
def __post_init__(self):
self.texture = self._create_texture()
def _calculate_vertical_spacing(self, current_char, next_char=None):
# Check if current char is lowercase or whitespace
is_spacing_char = current_char.islower() or current_char.isspace() or current_char in self.no_space_chars
# Additional check for capitalization transition
if next_char and current_char.isupper() and next_char.islower() or next_char in self.no_space_chars:
is_spacing_char = True
# Apply spacing factor if it's a spacing character
if is_spacing_char:
return self.font_size * (self.line_spacing * self.lowercase_spacing_factor)
return self.font_size * self.line_spacing
def _draw_rotated_char(self, image, font, char, pos, font_size, color, is_outline=False):
# Calculate character size
char_size = ray.measure_text_ex(font, char, font_size, 1.0)
# Create a temporary image for the rotated character
temp_image = ray.gen_image_color(int(char_size.y), int(char_size.x), ray.Color(0, 0, 0, 0))
# Draw the character on the temporary image
ray.image_draw_text_ex(
temp_image,
font,
char,
ray.Vector2(0, 0),
font_size,
1.0,
color
)
# Rotate the temporary image 90 degrees
rotated_image = ray.gen_image_color(int(char_size.x), int(char_size.y), ray.Color(0, 0, 0, 0))
for x in range(int(char_size.y)):
for y in range(int(char_size.x)):
pixel = ray.get_image_color(temp_image, y, int(char_size.y) - x - 1)
ray.image_draw_pixel(
rotated_image,
x,
y,
pixel
)
# Unload temporary image
ray.unload_image(temp_image)
# Draw the rotated image
ray.image_draw(
image,
rotated_image,
ray.Rectangle(0, 0, rotated_image.width, rotated_image.height),
ray.Rectangle(int(pos.x), int(pos.y), rotated_image.width, rotated_image.height),
ray.WHITE
)
# Unload rotated image
ray.unload_image(rotated_image)
def _create_texture(self):
# Measure text size
text_size = ray.measure_text_ex(self.font, self.text, self.font_size, 1.0)
# Determine dimensions based on orientation
if not self.vertical:
width = int(text_size.x + self.outline_thickness * 4)
height = int(text_size.y + self.outline_thickness * 4)
padding_x, padding_y = self.outline_thickness * 2, self.outline_thickness * 2
else:
# For vertical text, calculate total height and max character width
char_heights = [
self._calculate_vertical_spacing(
self.text[i],
self.text[i+1] if i+1 < len(self.text) else None
)
for i in range(len(self.text))
]
# Calculate the maximum character width (including outline)
char_widths = []
for char in self.text:
if char in self.vertical_chars:
# For vertically drawn characters, use font size as width
char_width = self.font_size
else:
# Normal character width
char_width = ray.measure_text_ex(self.font, char, self.font_size, 1.0).x
char_widths.append(char_width)
max_char_width = max(char_widths) if char_widths else 0
total_height = sum(char_heights) if char_heights else 0
# Adjust dimensions to be tighter around the text
width = int(max_char_width + self.outline_thickness * 2) # Reduced padding
height = int(total_height + self.outline_thickness * 2) # Reduced padding
padding_x = self.outline_thickness
padding_y = self.outline_thickness
# Create transparent image
image = ray.gen_image_color(width, height, ray.Color(0, 0, 0, 0))
# Draw outline
if not self.vertical:
# Horizontal text outline
for dx in range(-self.outline_thickness, self.outline_thickness + 1):
for dy in range(-self.outline_thickness, self.outline_thickness + 1):
if dx == 0 and dy == 0:
continue
ray.image_draw_text_ex(
image,
self.font,
self.text,
ray.Vector2(padding_x + dx, padding_y + dy),
self.font_size,
1.0,
self.outline_color
)
else:
# Vertical text outline
current_y = padding_y
for dx in range(-self.outline_thickness, self.outline_thickness + 1):
for dy in range(-self.outline_thickness, self.outline_thickness + 1):
if dx == 0 and dy == 0:
continue
current_y = padding_y
for i, char in enumerate(self.text):
if char in self.vertical_chars:
char_width = self.font_size
else:
char_width = ray.measure_text_ex(self.font, char, self.font_size, 1.0).x
# Calculate centered position
center_offset = (width - char_width) // 2
char_height = self._calculate_vertical_spacing(
char,
self.text[i+1] if i+1 < len(self.text) else None
)
if char in self.vertical_chars:
self._draw_rotated_char(
image,
self.font,
char,
ray.Vector2(
center_offset + dx,
current_y + dy
),
self.font_size,
self.outline_color,
is_outline=True
)
else:
ray.image_draw_text_ex(
image,
self.font,
char,
ray.Vector2(center_offset + dx, current_y + dy),
self.font_size,
1.0,
self.outline_color
)
current_y += char_height
# Draw main text
if not self.vertical:
# Horizontal text
ray.image_draw_text_ex(
image,
self.font,
self.text,
ray.Vector2(padding_x, padding_y),
self.font_size,
1.0,
self.text_color
)
else:
# Vertical text
current_y = padding_y
for i, char in enumerate(self.text):
if char in self.vertical_chars:
char_width = self.font_size
else:
char_width = ray.measure_text_ex(self.font, char, self.font_size, 1.0).x
# Calculate centered position
center_offset = (width - char_width) // 2
char_height = self._calculate_vertical_spacing(
char,
self.text[i+1] if i+1 < len(self.text) else None
)
if char in self.vertical_chars:
self._draw_rotated_char(
image,
self.font,
char,
ray.Vector2(
center_offset,
current_y
),
self.font_size,
self.text_color
)
else:
ray.image_draw_text_ex(
image,
self.font,
char,
ray.Vector2(center_offset, current_y),
self.font_size,
1.0,
self.text_color
)
current_y += char_height
# Create texture and clean up
texture = ray.load_texture_from_image(image)
ray.unload_image(image)
return texture
def draw(self, src: ray.Rectangle, dest: ray.Rectangle, origin: ray.Vector2, rotation: float, color: ray.Color):
ray.draw_texture_pro(self.texture, src, dest, origin, rotation, color)
def unload(self):
ray.unload_texture(self.texture)