#!/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()