diff options
Diffstat (limited to 'pygame')
-rw-r--r-- | pygame/Makefile | 23 | ||||
-rwxr-xr-x | pygame/__init__.py | 695 | ||||
-rw-r--r-- | pygame/arrow-right.png | bin | 2840 -> 0 bytes | |||
-rwxr-xr-x | pygame/gamechestgui | 10 | ||||
-rwxr-xr-x | pygame/main.py | 441 |
5 files changed, 728 insertions, 441 deletions
diff --git a/pygame/Makefile b/pygame/Makefile new file mode 100644 index 0000000..36729a7 --- /dev/null +++ b/pygame/Makefile @@ -0,0 +1,23 @@ +.PHONY: all +all: deploy + +#.PHONY: zipapp +#zipapp: +# rm -rf /tmp/gamechestgui.zipapp +# mkdir /tmp/gamechestgui.zipapp +# pip3 install --target /tmp/gamechestgui.zipapp -r ../gamechestcli/requirements.txt +# pip3 install --target /tmp/gamechestgui.zipapp -r requirements.txt +# rsync -Pa ../gamechestcli/gamechest /tmp/gamechestgui.zipapp/. +# rsync -Pa __main__.py profiledb.py /tmp/gamechestgui.zipapp/. +# python3 -m zipapp -p "/usr/bin/env python3" /tmp/gamechestgui.zipapp +# if [ -e ~/games/.saves ]; then mv /tmp/gamechestgui.pyz ~/games/.saves/tools/bin/. ; elif [ -e ~/game-saves ]; then mv /tmp/gamechestgui.pyz ~/game-saves/tools/bin/. ; fi + +.PHONY: deploy +deploy: + rm -rf ~/games/.saves/tools/bin/libs/gamechestgui + mkdir -p ~/games/.saves/tools/bin/libs/gamechestgui + pip3 install --target ~/games/.saves/tools/bin/libs/gamechestgui -r ../gamechestcli/requirements.txt + pip3 install --target ~/games/.saves/tools/bin/libs/gamechestgui -r requirements.txt + rsync -Pa ../gamechestcli/gamechest ~/games/.saves/tools/bin/libs/gamechestgui/. + rsync -Pa __init__.py profiledb.py ~/games/.saves/tools/bin/libs/gamechestgui/. + rsync -Pa gamechestgui ~/games/.saves/tools/bin/. diff --git a/pygame/__init__.py b/pygame/__init__.py new file mode 100755 index 0000000..b6ec930 --- /dev/null +++ b/pygame/__init__.py @@ -0,0 +1,695 @@ +#!/usr/bin/python3 + +from dataclasses import dataclass +from functools import lru_cache, partial +from pathlib import Path +import subprocess +import selectors + +#import pygame_sdl2 as pygame +import pygame +import yaml + +from .profiledb import ProfileDB + +from gamechest.cliactions import run +from gamechest.gamedb import GameDB +from gamechest.statusdb import StatusDB +from gamechest.gameconfig import config +from gamechest.runners.install import Install +from gamechest.runners.remove import Remove + + +# 16:9 ratio +#WINDOW_RESOLUTION = (1280, 720) +#WINDOW_RESOLUTION = (800, 600) +WINDOW_RESOLUTION = (1024, 576) + +if pygame.ver[0] != '2': + raise ValueError(f'pygame2 required, got {pygame.ver}') + + +class Event(Exception): pass +class QuitEvent(Event): pass + + +@lru_cache(maxsize=40) +def get_text_surface(text, font, color): + new_surface = font.render(text, Parameters.font_antialias, color) + print('cache miss for', text) + return new_surface + + +def calculate_new_size(size_max, size): + width_max, height_max = size_max + width, height = size + + if width > width_max: + ratio = width_max / width + width = width_max + height = height * ratio + + if height > height_max: + ratio = height_max / height + height = height_max + width = width * ratio + + return width, height + + +@lru_cache(maxsize=40) +def get_image_surface(path, resize=None, alpha=False): + print('cache miss for', path) + image = pygame.image.load(str(path)) + if alpha: + image = image.convert_alpha() + else: + image = image.convert() + if resize: + new_size = calculate_new_size(resize, (image.get_width(), + image.get_height())) + image = pygame.transform.scale(image, new_size) + print('new_size', new_size) + return image + + +@dataclass +class AppState: + menu_selected_index: int = 0 + + +class Parameters: + key_delay = 330 + key_interval = 1000//25 + font_antialias = True + + + + +class GuiApp: + + def __init__(self): + pygame.init() + pygame.key.set_repeat(Parameters.key_delay, Parameters.key_interval) + #print(pygame.display.list_modes()) + self.window = pygame.display.set_mode(WINDOW_RESOLUTION) + self.clock = pygame.time.Clock() + self.cycles_per_second = 60 + self.menu_font = pygame.font.Font(pygame.font.get_default_font(), 16) + self.menu_color = (0, 0, 200) + self.render_timer = 0 + self.render_timeout = 1000//30 # 30 fps + + def process_input(self, last_frame_time, event=None): + if event is None: + event = pygame.event.poll() + if event.type == pygame.KEYDOWN: + print('event', event, 'key name', pygame.key.name(event.key)) + if event.key in (pygame.K_q, pygame.K_ESCAPE): + raise QuitEvent() + if event.type == pygame.QUIT: + raise QuitEvent() + + def update(self, last_frame_time): + pass + + def render(self, last_frame_time): + self.render_timer += last_frame_time + if self.render_timer < self.render_timeout: + return + self.render_timer = 0 + + self.window.fill((0, 0, 0)) + + # Initial y + y = 50 + x = 50 + + self.window.blit(surface, (x, y)) + + pygame.display.update() + + + def loop(self): + last_frame_time = 1 # equivalent to 0 but avoid div0 + last_render_time = 1 + try: + while 1: + self.process_input(last_frame_time) + self.update(last_frame_time) + self.render(last_frame_time) + last_frame_time = self.clock.tick(self.cycles_per_second) + except QuitEvent: + pass + + print('quitting') + pygame.quit() + + +class ProfileDataObserver: + + def event_current_profile_changed(self, new_profile): + pass + + +class ObservedSubject: + def __init__(self): + self.observers = [] + + def add_observer(self, observer): + self.observers.append(observer) + + def del_observer(self, observer): + self.observers.remove(observer) + + def notify_event(self, event_name, *args, **kws): + for observer in self.observers: + getattr(observer, event_name)(*args, **kws) + + +class ProfileData(ObservedSubject): + + def __init__(self): + super().__init__() + self.profiledb = ProfileDB() + self.profiles_len = len(self.profiledb.get_profiles()) + self.current_profile_index = 0 + configured_default_profile = config.get_profile_id() + for index, profile in enumerate(self.profiledb.get_profiles()): + if profile['name'] == configured_default_profile: + self.current_profile_index = index + + def change_active_profile(self, new_index): + if new_index < 0: + new_index = 0 + elif new_index >= self.profiles_len: + new_index = self.profiles_len-1 + if self.current_profile_index != new_index: + self.current_profile_index = new_index + self.notify_event('event_current_profile_changed', + self.get_current_profile()) + + def change_profile_increase(self): + self.change_active_profile(self.current_profile_index +1) + + def change_profile_decrease(self): + self.change_active_profile(self.current_profile_index -1) + + def get_current_profile(self): + return self.profiledb.get_profiles()[self.current_profile_index]['name'] + + def get_current_profile_avatar(self): + return self.profiledb.get_profiles()[self.current_profile_index].get('avatar', None) + + def get_current_display(self): + return self.profiledb.get_profiles()[self.current_profile_index]['display'] + + +class ProfileRenderer(ProfileDataObserver): + + def __init__(self, profiledata): + self.profiledata = profiledata + self.surface_to_render = None + profiledata.add_observer(self) + self.font_filename = pygame.font.match_font('bitstreamverasans') + print('ProfileRenderer font:', self.font_filename) + self.font = pygame.font.Font(self.font_filename, 24) + self.font_fg_color = (0, 200, 200) + self.avatar_surface = None + self.avatar_size = (100, 100) + self.image_box = get_image_surface(config.get_data_d() / 'asset/box.png', resize=(170, 170), alpha=True) + self.load_avatar() + + def load_avatar(self): + self.avatar_surface = None + if path := self.profiledata.get_current_profile_avatar(): + path = config.get_data_d() / path + if path.exists(): + self.avatar_surface = get_image_surface(path, + self.avatar_size) + else: + print('no avatar found at', path) + + def event_current_profile_changed(self, new_profile): + self.surface_to_render = None + self.load_avatar() + print('changing rendering profile to', new_profile) + + def render(self, surface, pos): + if self.surface_to_render is None: + profile_name = self.profiledata.get_current_profile() + new_surface = get_text_surface( + self.profiledata.get_current_display(), + self.font, self.font_fg_color) + #new_surface = self.font.render(self.profiledata.get_current_display(), Parameters.font_antialias, self.font_fg_color) + self.surface_to_render = new_surface + avatar_pos = (pos[0] + 15, pos[1] + 15) + #text_pos = (avatar_pos[0] + self.avatar_size[0] + 5, avatar_pos[1]) + text_pos = (avatar_pos[0], avatar_pos[1] + self.avatar_size[1]) + surface.blit(self.image_box, pos) + if self.avatar_surface: + surface.blit(self.avatar_surface, avatar_pos) + surface.blit(self.surface_to_render, text_pos) + + + +class GameData(ObservedSubject): + + def __init__(self): + super().__init__() + self.game_db = GameDB() + self.games_idtitles = list(self.game_db.get_idtitles()) + self.games_len = len(self.games_idtitles) + self.game_index = 0 + + def change_active_game(self, new_index): + if new_index < 0: + new_index = 0 + elif new_index >= self.games_len: + new_index = self.games_len-1 + if self.game_index != new_index: + self.game_index = new_index + self.notify_event('event_current_game_changed', + self.get_current_gameinfo()) + + #def get_idtitles(self): + # return self.games_idtitles + + def get_games_subset(self): + low_index = max(self.game_index-3, 0) + high_index = low_index + 6 + for index, item in enumerate(self.games_idtitles[low_index:high_index], start=low_index): + selected = 1 if self.game_index == index else 0 + game_info = self.game_db.get_info(item[0]) + current_icon = game_info.get( + 'icon', None) + if current_icon is not None: + current_icon = config.get_data_d() / current_icon + yield selected, item[0], item[1], current_icon, game_info + + def get_current_gameinfo(self): + return self.game_db.get_info(self.get_current_id()) + + def get_current_id(self): + current_id = self.games_idtitles[self.game_index][0] + return current_id + + def get_current_title(self): + current_title = self.games_idtitles[self.game_index][1] + return current_title + + def change_game_increase(self): + self.change_active_game(self.game_index +1) + + def change_game_decrease(self): + self.change_active_game(self.game_index -1) + +class GameDataObserver: + + def event_current_game_changed(self, new_game_info): pass + +class GameRenderer(GameDataObserver): + + def __init__(self, gamedata, status_db): + self.gamedata = gamedata + self.status_db = status_db + gamedata.add_observer(self) + self.font_filename = pygame.font.match_font('bitstreamverasans') + print('GameRenderer font:', self.font_filename) + self.font = pygame.font.Font(self.font_filename, 16) + self.font_selected = pygame.font.Font(self.font_filename, 24) + self.font_fg_color = (90, 90, 200) + #self.image_right_arrow = get_image_surface(config.get_data_d() / 'asset/arrow-right.png', + # resize=(16, 16), + # alpha=True) + self.image_right_arrow = get_image_surface(config.get_data_d() / 'asset/bulb.png', + resize=(16, 16), + alpha=True) + self.image_box = get_image_surface(config.get_data_d() / 'asset/box.png', resize=(400, 400), alpha=True) + self.image_box.set_alpha(220) + + def event_current_game_changed(self, new_game_info): + print('game changed, new game:', new_game_info['title']) + + def render(self, dest_surface, pos): + x = pos[0] + y = pos[1] + y_pad = 32 + surface_height = None + #selected_item_y = y + #low_index = max(selected_index-3, 0) + #high_index = low_index + 6 + dest_surface.blit(self.image_box, pos) + x += 15 + y += 45 + for selected, game_id, game_title, game_icon, game_info in self.gamedata.get_games_subset(): + text = game_title + if not self.status_db.is_installed(game_info): + text = f'[u] {text}' + else: + text = f'[i] {text}' + arrow_width = self.image_right_arrow.get_width() + text_x = x + y_pad + surface = get_text_surface(text, + self.font_selected if selected else + self.font, + self.font_fg_color) + if game_icon and game_icon.exists(): + icon_surface = get_image_surface(game_icon, + resize=(y_pad, y_pad), + alpha=True) + icon_pos = (x + arrow_width, + y + (surface.get_height() + - icon_surface.get_height())/2) + + dest_surface.blit(icon_surface, icon_pos) + dest_surface.blit(surface, (text_x + + arrow_width + 5, y)) + if selected: + surface_height = surface.get_height() + #selected_item_y = y + dest_surface.blit(self.image_right_arrow, (x, y + (surface_height - self.image_right_arrow.get_height())/2)) + y += surface.get_height() + y_pad + + +class GameCoverRenderer(GameDataObserver): + + def __init__(self, gamedata): + self.gamedata = gamedata + gamedata.add_observer(self) + self.current_game_info = self.gamedata.get_current_gameinfo() + self.cover_shift = (0, 0) + self.default_cover_surface = get_image_surface(config.get_data_d() / 'asset/default_cover.jpeg', + resize=WINDOW_RESOLUTION) + self.default_cover_shift = ( + (WINDOW_RESOLUTION[0] - self.default_cover_surface.get_width())/2, + (WINDOW_RESOLUTION[1] - self.default_cover_surface.get_height())/2, + ) + self.load_cover() + + def event_current_game_changed(self, new_game_info): + print('game changed, cover:', new_game_info.get('cover', None)) + self.current_game_info = new_game_info + self.load_cover() + + def load_cover(self): + self.cover_surface = None + path = self.current_game_info.get('cover', None) + if path is None: + self.cover_surface = self.default_cover_surface + self.cover_shift = self.default_cover_shift + return + path = config.get_data_d() / Path(path) + if path.exists(): + self.cover_surface = get_image_surface(path, + resize=WINDOW_RESOLUTION) + self.cover_shift = ( + (WINDOW_RESOLUTION[0] - self.cover_surface.get_width())/2, + (WINDOW_RESOLUTION[1] - self.cover_surface.get_height())/2, + ) + else: + self.cover_surface = self.default_cover_surface + self.cover_shift = self.default_cover_shift + + def render(self, dest_surface, pos): + if self.cover_surface is None: + return + pos = ( + pos[0] + self.cover_shift[0], + pos[1] + self.cover_shift[1], + ) + dest_surface.blit(self.cover_surface, pos) + + +class ProgressBarRenderer: + + def __init__(self): + self.progress = .0 + self.back_color = 0xEEEEEEF0 + self.front_color = 0x0000FF00 + + def set_progress(self, progress): + 'progress is between .0 and 1.0' + self.progress = progress + + def render(self, dest_surface, pos): + size = (100, 10) + background_rect = pygame.Rect(pos[0], pos[1], size[0], size[1]) + inside_rect = pygame.Rect(pos[0]+2, pos[1]+2, int(96*self.progress), 6) + + dest_surface.lock() + pygame.draw.rect(dest_surface, self.back_color, background_rect, + border_radius=5) + pygame.draw.rect(dest_surface, self.front_color, inside_rect, + border_radius=5) + dest_surface.unlock() + + +class DoubleProgressBarRenderer: + def __init__(self): + self.progress_bar_global = ProgressBarRenderer() + self.progress_bar_step = ProgressBarRenderer() + self.title = "" + self.title_surface = None + self.font_filename = pygame.font.match_font('bitstreamverasans') + print('GameRenderer font:', self.font_filename) + self.font = pygame.font.Font(self.font_filename, 10) + self.font_color = 0xFFFFFFFF + self.set_title("") + + def set_progress(self, global_progress, step_progress): + self.progress_bar_global.set_progress(global_progress) + self.progress_bar_step.set_progress(step_progress) + + def set_title(self, title): + self.title = title or "" + self.title_surface = get_text_surface(title, self.font, self.font_color) + + def render(self, dest_surface, pos): + x = pos[0] + y = pos[1] + rect = pygame.Rect(x, y, 104, 10+10+10+2+2+2+2) + x += 2 + y += 2 + pygame.draw.rect(dest_surface, 0x00000000, rect, border_radius=3) + if self.title_surface: + dest_surface.blit(self.title_surface, (x, y)) + #y += self.title_surface.get_height() + 2 + y += 10 + 2 + self.progress_bar_global.render(dest_surface, (x, y)) + self.progress_bar_step.render(dest_surface, (x, y+12)) + + + +class GuiAppSub1(GuiApp): + + def __init__(self): + super().__init__() + self.input_timer = 0 + self.input_timeout = 1000//10 + self.state = AppState() + self.base_window_fill_color = (0, 0, 0) + self.user_move_vector = (0, 0) + self.game_db = GameDB() + #self.menu_font_cache = ({}, []) + #self.menu_font_cache_max = 40 + #self.last_movement_update_time = self.clock. + self.joysticks = {} + self.joysticks_axis_threshold = 0 + self.profile_data = ProfileData() + self.profile_renderer = ProfileRenderer(self.profile_data) + self.game_data = GameData() + self.status_db = StatusDB() + self.game_renderer = GameRenderer(self.game_data, self.status_db) + self.game_cover_renderer = GameCoverRenderer(self.game_data) + self.action = None + self.runner = None + self.progress_bar = DoubleProgressBarRenderer() + self.selector = selectors.DefaultSelector() + self.delay_change = None + + def process_input(self, last_frame_time): + + movement_up_keys = ( + pygame.K_UP, + pygame.K_k, + ) + movement_down_keys = ( + pygame.K_DOWN, + pygame.K_j, + ) + axis_change = 0 + for event in pygame.event.get(): + go_super = 1 + x, y = self.user_move_vector + if event.type == pygame.KEYDOWN: + if event.key in (*movement_up_keys, *movement_down_keys): + y = 1 if event.key in movement_down_keys else -1 + go_super = 0 + elif event.key in (pygame.K_LEFT, pygame.K_RIGHT): + x = -1 if event.key == pygame.K_LEFT else 1 + go_super = 0 + elif event.type == pygame.KEYUP: + x, y = self.user_move_vector + if event.key in (*movement_up_keys, *movement_down_keys): + y = 0 + go_super = 0 + elif event.key in (pygame.K_LEFT, pygame.K_RIGHT): + x = 0 + go_super = 0 + elif event.key == pygame.K_RETURN: + self.action = 'run' + elif event.key == pygame.K_i: + self.action = 'install' + elif event.key == pygame.K_d: + self.action = 'remove' + elif event.type in (pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP): + go_super = 0 + factor = 1 if pygame.JOYBUTTONDOWN else 0 + joystick = self.joysticks[event.instance_id] + print('gamepad event', event) + #if event.button == 0: + #y += 1 * factor + if event.type == pygame.JOYBUTTONDOWN: + joystick.rumble(0.3, 0.8, 40) + if event.button == 1: + raise QuitEvent() + elif event.button == 2: + self.action = 'run' + #if event.button == 1: + #y -= 1 * factor + elif event.type == pygame.JOYAXISMOTION: + go_super = 0 + if event.axis == 1: # left stick up-down axis + new_axis = self.joysticks_axis_threshold + #print('motion event', event) + if event.value > 0.70: + new_axis = 1 # down activated + elif event.value < 0.5 and event.value > -0.5: + new_axis = 0 # down and up deactivated + elif event.value < -0.70: + new_axis = -1 # up activated + if new_axis != self.joysticks_axis_threshold: + axis_change = 1 + self.joysticks_axis_threshold = new_axis + print('threshold', self.joysticks_axis_threshold) + elif event.type == pygame.JOYDEVICEADDED: + # This event will be generated when the program starts for every + # joystick, filling up the list without needing to create them manually. + joy = pygame.joystick.Joystick(event.device_index) + self.joysticks[joy.get_instance_id()] = joy + print(f"Gamepad {joy.get_instance_id()} connected") + elif event.type == pygame.JOYDEVICEREMOVED: + del self.joysticks[event.instance_id] + print(f"Gamepad {event.instance_id} disconnected") + + if go_super == 0: + if axis_change: + self.user_move_vector = (x, self.joysticks_axis_threshold) + else: + self.user_move_vector = (x, y) + #print('new move vector:', self.user_move_vector) + else: + super().process_input(last_frame_time, event) + + def update(self, last_frame_time): + #self.input_timer += last_frame_time + #if self.input_timer < self.input_timeout: + # return + #self.input_timer = 0 + + if self.user_move_vector[1] < 0: + self.game_data.change_game_decrease() + elif self.user_move_vector[1] > 0: + self.game_data.change_game_increase() + + if self.user_move_vector[0] < 0: + self.profile_data.change_profile_decrease() + elif self.user_move_vector[0] > 0: + self.profile_data.change_profile_increase() + + if action := self.action: + self.action = None + profile_id = self.profile_renderer.profiledata.get_current_profile() + game_id = self.game_data.get_current_id() + game_info = self.game_data.get_current_gameinfo() + print('current id', game_id) + print('profile id', profile_id) + if not self.runner: + # action not already running, else skip action + if action == 'run': + if not self.status_db.is_installed(game_info): + print('Game', game_id, 'is not installed, aborting.', file=sys.stderr) + else: + command = self.game_db.get_game_command(profile_id, game_id) + subprocess.run(command) + elif action == 'install': + #subprocess.run([ + # 'gamechest', + # 'install', + # game_id]) + remote_basedir = config.get_remote_basedir() + source = f'{remote_basedir}/{game_info["package_name"]}' + dest = config.get_games_install_basedir() / game_info['id'] + dest.mkdir(parents=True, exist_ok=True) + self.runner = Install(source, dest) + self.selector.register(self.runner.get_read_fd(), + selectors.EVENT_READ) + self.delay_change = partial(self.status_db.set_installed, game_info) + self.progress_bar.set_title("installing...") + elif action == 'remove': + remote_basedir = config.get_remote_basedir() + path = ( + config.get_games_install_basedir() + / game_id + ) + #subprocess.run([ + # 'gamechest', + # 'remove', + # game_id]) + self.runner = Remove(path) + self.selector.register(self.runner.get_read_fd(), + selectors.EVENT_READ) + self.delay_change = partial(self.status_db.set_uninstalled, game_info) + self.progress_bar.set_title("removing...") + + if self.runner: + if self.runner.poll() is None: + if self.selector.select(0): + progress = self.runner.progress_read() + self.progress_bar.set_progress(progress.step / progress.steps_count, progress.percent / 100) + else: + self.progress_bar.set_progress(1, 1) + self.progress_bar.set_title("done") + self.runner = None + if self.delay_change is not None: + self.delay_change() + self.delay_change = None + + + self.user_move_vector = (0, 0) + + def render(self, last_frame_time): + self.window.fill(self.base_window_fill_color) + + self.game_cover_renderer.render(self.window, (0, 0)) + + # Initial y + y = 50 + x = 50 + 64 + 5 + self.game_renderer.render(self.window, (x, y)) + + + fps_surface = get_text_surface(f'{self.clock.get_fps():02.0f}', + self.menu_font, self.menu_color) + fps_pos = (self.window.get_width() - fps_surface.get_width(), 0) + self.window.blit(fps_surface, fps_pos) + + self.profile_renderer.render(self.window, (700, 50)) + self.progress_bar.render(self.window, (500, 50)) + + pygame.display.update() + + +app = GuiAppSub1() +app.loop() diff --git a/pygame/arrow-right.png b/pygame/arrow-right.png Binary files differdeleted file mode 100644 index c88334f..0000000 --- a/pygame/arrow-right.png +++ /dev/null diff --git a/pygame/gamechestgui b/pygame/gamechestgui new file mode 100755 index 0000000..5cedfaa --- /dev/null +++ b/pygame/gamechestgui @@ -0,0 +1,10 @@ +#!/usr/bin/python3 + +import sys +import os +file_path = os.path.realpath(__file__) +sys.path.insert(0, os.path.join(os.path.dirname(file_path), 'libs')) +sys.path.insert(0, os.path.join(os.path.dirname(file_path), 'libs/gamechestgui')) + +import gamechestgui +#gamechestgui.__main__() diff --git a/pygame/main.py b/pygame/main.py deleted file mode 100755 index ba61077..0000000 --- a/pygame/main.py +++ /dev/null @@ -1,441 +0,0 @@ -#!/usr/bin/python3 - -from dataclasses import dataclass -from pathlib import Path - -#import pygame_sdl2 as pygame -import pygame -import yaml - -from profiledb import ProfileDB - -from gamechest.cliactions import run -from gamechest.gamedb import GameDB - - -#WINDOW_RESOLUTION = (1280, 720) -WINDOW_RESOLUTION = (800, 600) - -if pygame.ver[0] != '2': - raise ValueError(f'pygame2 required, got {pygame.ver}') - - -class Event(Exception): pass -class QuitEvent(Event): pass - - -@dataclass -class AppState: - menu_selected_index: int = 0 - - -class Parameters: - key_delay = 330 - key_interval = 1000//25 - - - - -class GuiApp: - - def __init__(self): - pygame.init() - pygame.key.set_repeat(Parameters.key_delay, Parameters.key_interval) - #print(pygame.display.list_modes()) - self.window = pygame.display.set_mode(WINDOW_RESOLUTION) - self.clock = pygame.time.Clock() - self.cycles_per_second = 60 - #self.render_factor_div = 2 # 60/2 => 30fps - #self.input_factor_div = 6 # 60/6 => 10 input repeat per second - self.menu_font = pygame.font.Font(pygame.font.get_default_font(), 32) - self.font_antialias = True - #self.font_antialias = False - self.menu_color = (0, 0, 200) - self.render_timer = 0 - self.render_timeout = 1000//30 # 30 fps - - def process_input(self, last_frame_time, event=None): - if event is None: - event = pygame.event.poll() - if event.type == pygame.KEYDOWN: - print('event', event, 'key name', pygame.key.name(event.key)) - if event.key in (pygame.K_q, pygame.K_ESCAPE): - raise QuitEvent() - if event.type == pygame.QUIT: - raise QuitEvent() - - def update(self, last_frame_time): - pass - - def render(self, last_frame_time): - self.render_timer += last_frame_time - if self.render_timer < self.render_timeout: - return - self.render_timer = 0 - - self.window.fill((0, 0, 0)) - - # Initial y - y = 50 - - # Title - #surface = self.titleFont.render("TANK BATTLEGROUNDS !!", True, (200, 0, 0)) - #x = (self.window.get_width() - surface.get_width()) // 2 - #self.window.blit(surface, (x, y)) - #y += (200 * surface.get_height()) // 100 - - x = 50 - - # Compute menu width - #menuWidth = 0 - #for item in self.menuItems: - # surface = self.itemFont.render(item['title'], True, (200, 0, 0)) - # menuWidth = max(menuWidth, surface.get_width()) - # item['surface'] = surface - - surface = self.menu_font.render("I love my cat !", - self.font_antialias, self.menu_color) - self.window.blit(surface, (x, y)) - - ## Draw menu items - #x = (self.window.get_width() - menuWidth) // 2 - #for index, item in enumerate(self.menuItems): - # # Item text - # surface = item['surface'] - # self.window.blit(surface, (x, y)) - - # # Cursor - # if index == self.currentMenuItem: - # cursorX = x - self.menuCursor.get_width() - 10 - # cursorY = y + (surface.get_height() - self.menuCursor.get_height()) // 2 - # self.window.blit(self.menuCursor, (cursorX, cursorY)) - - # y += (120 * surface.get_height()) // 100 - - - #pygame.draw.rect(self.window, - # (0,0,255), - # (120,120,400,240)) - - pygame.display.update() - - - def loop(self): - #current_render_div = self.render_factor_div - #current_input_div = self.input_factor_div - last_frame_time = 1 # equivalent to 0 but avoid div0 - last_render_time = 1 - try: - while 1: - #current_render_div -= 1 - #current_input_div -= 1 - self.process_input(last_frame_time) - #self.update(current_input_div) - self.update(last_frame_time) - self.render(last_frame_time) - #if current_render_div <= 0: - # self.render(last_render_time) - # current_render_div = self.render_factor_div - # last_render_time = 0 - #if current_input_div <= 0: - # current_input_div = self.input_factor_div - last_frame_time = self.clock.tick(self.cycles_per_second) - #last_render_time += last_frame_time - except QuitEvent: - pass - - print('quitting') - pygame.quit() - - -class ProfileDataObserver: - - def event_current_profile_changed(self, new_profile): - pass - - -class ObservedSubject: - def __init__(self): - self.observers = [] - - def add_observer(self, observer): - self.observers.append(observer) - - def del_observer(self, observer): - self.observers.remove(observer) - - def notify_event(self, event_name, *args, **kws): - for observer in self.observers: - getattr(observer, event_name)(*args, **kws) - - -class ProfileData(ObservedSubject): - - def __init__(self): - super().__init__() - self.profiledb = ProfileDB() - self.profiles_len = len(self.profiledb.get_profiles()) - self.current_profile_index = 0 - - def change_active_profile(self, new_index): - if new_index < 0: - new_index = 0 - elif new_index >= self.profiles_len: - new_index = self.profiles_len-1 - if self.current_profile_index != new_index: - print('new index', self.current_profile_index, '->', new_index) - self.current_profile_index = new_index - print(id(self), 'new index', self.current_profile_index) - self.notify_event('event_current_profile_changed', - self.get_current_profile()) - - def change_profile_increase(self): - self.change_active_profile(self.current_profile_index +1) - - def change_profile_decrease(self): - self.change_active_profile(self.current_profile_index -1) - - def get_current_profile(self): - return self.profiledb.get_profiles()[self.current_profile_index]['name'] - - def get_current_display(self): - return self.profiledb.get_profiles()[self.current_profile_index]['display'] - - -class ProfileRenderer(ProfileDataObserver): - - def __init__(self, profiledata): - self.profiledata = profiledata - self.surface_to_render = None - profiledata.add_observer(self) - self.font_filename = pygame.font.match_font('bitstreamverasans') - print('ProfileRenderer font:', self.font_filename) - self.font = pygame.font.Font(self.font_filename, 24) - self.font_antialias = True - self.font_fg_color = (0, 200, 200) - - def event_current_profile_changed(self, new_profile): - self.surface_to_render = None - print('changing rendering profile to', new_profile) - - def render(self, surface): - if self.surface_to_render is None: - profile_name = self.profiledata.get_current_profile() - new_surface = self.font.render(self.profiledata.get_current_display(), self.font_antialias, self.font_fg_color) - self.surface_to_render = new_surface - surface.blit(self.surface_to_render, (50, 50)) - - -class GuiAppSub1(GuiApp): - - def __init__(self): - super().__init__() - self.input_timer = 0 - self.input_timeout = 1000//10 - self.state = AppState() - self.base_window_fill_color = (0, 0, 0) - self.user_move_vector = (0, 0) - #with Path('~/games/.saves/gamedata.yaml').expanduser().open() as fp: - # self.gamedb = yaml.safe_load(fp) - self.game_db = GameDB() - #self.menu_items = [x['title'] for x in self.gamedb['games']] - self.menu_items = list(self.game_db.get_ids()) - self.menu_font_cache = ({}, []) - self.menu_font_cache_max = 40 - self.image_right_arrow = pygame.image.load('arrow-right.png').convert() - #self.last_movement_update_time = self.clock. - self.joysticks = {} - self.joysticks_axis_threshold = 0 - self.profile_data = ProfileData() - self.profile_renderer = ProfileRenderer(self.profile_data) - - - def menu_font_render(self, text): - cached_surface = self.menu_font_cache[0].get(text, None) - if cached_surface is not None: - self.menu_font_cache[1].remove(text) - self.menu_font_cache[1].append(text) # refresh mru place - return cached_surface - - new_surface = self.menu_font.render(text, - self.font_antialias, - self.menu_color) - - self.menu_font_cache[0][text] = new_surface - self.menu_font_cache[1].append(text) - - if len(self.menu_font_cache[1]) > self.menu_font_cache_max: - removed_item_text = self.menu_font_cache[1].pop(0) - del self.menu_font_cache[0][removed_item_text] - - # debug for cache miss: - print('cache miss for', text) - - return new_surface - - def process_input(self, last_frame_time): - - movement_up_keys = ( - pygame.K_UP, - pygame.K_k, - ) - movement_down_keys = ( - pygame.K_DOWN, - pygame.K_j, - ) - axis_change = 0 - for event in pygame.event.get(): - go_super = 1 - x, y = self.user_move_vector - if event.type == pygame.KEYDOWN: - if event.key in (*movement_up_keys, *movement_down_keys): - y = 1 if event.key in movement_down_keys else -1 - go_super = 0 - elif event.key in (pygame.K_LEFT, pygame.K_RIGHT): - x = -1 if event.key == pygame.K_LEFT else 1 - go_super = 0 - elif event.type == pygame.KEYUP: - x, y = self.user_move_vector - if event.key in (*movement_up_keys, *movement_down_keys): - y = 0 - go_super = 0 - elif event.key in (pygame.K_LEFT, pygame.K_RIGHT): - x = 0 - go_super = 0 - elif event.type in (pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP): - go_super = 0 - factor = 1 if pygame.JOYBUTTONDOWN else 0 - joystick = self.joysticks[event.instance_id] - print('gamepad event', event) - #if event.button == 0: - #y += 1 * factor - if event.type == pygame.JOYBUTTONDOWN: - joystick.rumble(0.3, 0.8, 40) - if event.button == 1: - raise QuitEvent() - #if event.button == 1: - #y -= 1 * factor - elif event.type == pygame.JOYAXISMOTION: - go_super = 0 - if event.axis == 1: # left stick up-down axis - new_axis = self.joysticks_axis_threshold - #print('motion event', event) - if event.value > 0.70: - new_axis = 1 # down activated - elif event.value < 0.5 and event.value > -0.5: - new_axis = 0 # down and up deactivated - elif event.value < -0.70: - new_axis = -1 # up activated - if new_axis != self.joysticks_axis_threshold: - axis_change = 1 - self.joysticks_axis_threshold = new_axis - print('threshold', self.joysticks_axis_threshold) - elif event.type == pygame.JOYDEVICEADDED: - # This event will be generated when the program starts for every - # joystick, filling up the list without needing to create them manually. - joy = pygame.joystick.Joystick(event.device_index) - self.joysticks[joy.get_instance_id()] = joy - print(f"Gamepad {joy.get_instance_id()} connected") - elif event.type == pygame.JOYDEVICEREMOVED: - del self.joysticks[event.instance_id] - print(f"Gamepad {event.instance_id} disconnected") - - if go_super == 0: - if axis_change: - self.user_move_vector = (x, self.joysticks_axis_threshold) - else: - self.user_move_vector = (x, y) - print('new move vector:', self.user_move_vector) - else: - super().process_input(last_frame_time, event) - - def update(self, last_frame_time): - #self.input_timer += last_frame_time - #if self.input_timer < self.input_timeout: - # return - #self.input_timer = 0 - - menu_new_index = self.state.menu_selected_index - if self.user_move_vector[1] < 0: - menu_new_index -= 1 - elif self.user_move_vector[1] > 0: - menu_new_index += 1 - if menu_new_index < 0: - menu_new_index = 0 - elif menu_new_index > (len(self.menu_items)-1): - menu_new_index = len(self.menu_items)-1 - self.state.menu_selected_index = menu_new_index - - if self.user_move_vector[0] < 0: - self.profile_data.change_profile_decrease() - elif self.user_move_vector[0] > 0: - self.profile_data.change_profile_increase() - - self.user_move_vector = (0, 0) - - def render(self, last_frame_time): - self.window.fill(self.base_window_fill_color) - - # Initial y - y = 50 - - # Title - #surface = self.titleFont.render("TANK BATTLEGROUNDS !!", True, (200, 0, 0)) - #x = (self.window.get_width() - surface.get_width()) // 2 - #self.window.blit(surface, (x, y)) - #y += (200 * surface.get_height()) // 100 - - x = 50 + 64 + 5 - - # Compute menu width - #menuWidth = 0 - #for item in self.menuItems: - # surface = self.itemFont.render(item['title'], True, (200, 0, 0)) - # menuWidth = max(menuWidth, surface.get_width()) - # item['surface'] = surface - - y_pad = 5 - font_selected_item_height = None - selected_item_y = y - for index, item in enumerate(self.menu_items): - surface = self.menu_font_render(item) - self.window.blit(surface, (x, y)) - if self.state.menu_selected_index == index: - font_selected_item_height = surface.get_height() - selected_item_y = y - y += surface.get_height() + y_pad - - self.window.blit(self.image_right_arrow, (50, selected_item_y - 64//2 - + font_selected_item_height//2)) - - - fps_surface = self.menu_font_render(f'{self.clock.get_fps():02.0f}') - fps_pos = (self.window.get_width() - fps_surface.get_width(), 0) - self.window.blit(fps_surface, fps_pos) - - ## Draw menu items - #x = (self.window.get_width() - menuWidth) // 2 - #for index, item in enumerate(self.menuItems): - # # Item text - # surface = item['surface'] - # self.window.blit(surface, (x, y)) - - # # Cursor - # if index == self.currentMenuItem: - # cursorX = x - self.menuCursor.get_width() - 10 - # cursorY = y + (surface.get_height() - self.menuCursor.get_height()) // 2 - # self.window.blit(self.menuCursor, (cursorX, cursorY)) - - # y += (120 * surface.get_height()) // 100 - - - #pygame.draw.rect(self.window, - # (0,0,255), - # (120,120,400,240)) - self.profile_renderer.render(self.window) - - pygame.display.update() - - -app = GuiAppSub1() -app.loop() |