class MapEditorView: def __init__(self, pygame_screen, camera_world_area: Rect, screen_size: Tuple[int, int], images_by_sprite: Dict[Sprite, Dict[Direction, List[ImageWithRelativePosition]]], images_by_ui_sprite: Dict[UiIconSprite, Any], images_by_portrait_sprite: Dict[PortraitIconSprite, Any], world_area: Rect, player_position: Tuple[int, int], entities_by_type: Dict[EntityTab, List[MapEditorWorldEntity]], grid_cell_size: int, map_file_path: str): camera_size = camera_world_area.size self._camera_size = camera_size self._screen_size = screen_size self._screen_render = DrawableArea(pygame_screen) self._map_file_path = map_file_path self._images_by_sprite = images_by_sprite self._images_by_ui_sprite = images_by_ui_sprite self._images_by_portrait_sprite = images_by_portrait_sprite self._ui_screen_area = Rect(0, camera_size[1], screen_size[0], screen_size[1] - camera_size[1]) self._screen_render = DrawableArea(pygame_screen) self._ui_render = DrawableArea(pygame_screen, self._translate_ui_position_to_screen) self._font_debug_info = pygame.font.Font(DIR_FONTS + 'Courier New Bold.ttf', 12) self._font_ui_icon_keys = pygame.font.Font(DIR_FONTS + 'Courier New Bold.ttf', 12) w_tab_button = 110 self._tab_buttons_by_entity_type = { EntityTab.ADVANCED: RadioButton(self._ui_render, Rect(220, 10, w_tab_button, 20), "ADVANCED (C)"), EntityTab.ITEMS: RadioButton(self._ui_render, Rect(340, 10, w_tab_button, 20), "ITEMS (V)"), EntityTab.NPCS: RadioButton(self._ui_render, Rect(460, 10, w_tab_button, 20), "NPCS (B)"), EntityTab.WALLS: RadioButton(self._ui_render, Rect(580, 10, w_tab_button, 20), "WALLS (N)"), EntityTab.MISC: RadioButton(self._ui_render, Rect(700, 10, w_tab_button, 20), "MISC. (M)"), } self._tabs_by_pygame_key = { pygame.K_c: EntityTab.ADVANCED, pygame.K_v: EntityTab.ITEMS, pygame.K_b: EntityTab.NPCS, pygame.K_n: EntityTab.WALLS, pygame.K_m: EntityTab.MISC, } self._entities_by_type = entities_by_type self._tab_buttons = list(self._tab_buttons_by_entity_type.values()) self._minimap = Minimap(self._ui_render, Rect(self._screen_size[0] - 180, 20, 160, 160), world_area, player_position) self._shown_tab: EntityTab = None self._set_shown_tab(EntityTab.ADVANCED) self._checkboxes = [ Checkbox(self._ui_render, Rect(15, 100, 140, 20), "outlines", False, lambda checked: ToggleOutlines(checked)) ] self._buttons = [ Button(self._ui_render, Rect(15, 125, 140, 20), "Generate random", lambda: GenerateRandomMap()), Button(self._ui_render, Rect(15, 150, 140, 20), "Save", lambda: SaveMap()) ] # USER INPUT STATE self._is_left_mouse_button_down = False self._is_right_mouse_button_down = False self._mouse_screen_pos = (0, 0) self._hovered_component = None self._user_state: UserState = UserState.deleting_entities() self._snapped_mouse_screen_pos = (0, 0) self._snapped_mouse_world_pos = (0, 0) self._is_snapped_mouse_within_world = True self._is_snapped_mouse_over_ui = False self._entity_icon_hovered_by_mouse: MapEditorWorldEntity = None self._smart_floor_tiles_to_add: List[Tuple[int, int, int, int]] = [] self._smart_floor_tiles_to_delete: List[Tuple[int, int, int, int]] = [] # MUTABLE WORLD-RELATED STATE self.camera_world_area: Rect = camera_world_area self.world_area: Rect = world_area self.grid_cell_size = grid_cell_size self._setup_ui_components() def _setup_ui_components(self): icon_space = 5 x_1 = 165 y_2 = 40 self.button_delete_entities = self._create_map_editor_icon( Rect(20, y_2, MAP_EDITOR_UI_ICON_SIZE[0], MAP_EDITOR_UI_ICON_SIZE[1]), 'Q', None, UiIconSprite.MAP_EDITOR_TRASHCAN, 0, None) self.button_delete_decorations = self._create_map_editor_icon( Rect(20 + MAP_EDITOR_UI_ICON_SIZE[0] + icon_space, y_2, MAP_EDITOR_UI_ICON_SIZE[0], MAP_EDITOR_UI_ICON_SIZE[1]), 'Z', None, UiIconSprite.MAP_EDITOR_RECYCLING, 0, None) self.entity_icons_by_type = {} num_icons_per_row = 23 for entity_type in EntityTab: self.entity_icons_by_type[entity_type] = [] for i, entity in enumerate(self._entities_by_type[entity_type]): x = x_1 + (i % num_icons_per_row) * (MAP_EDITOR_UI_ICON_SIZE[0] + icon_space) row_index = (i // num_icons_per_row) y = y_2 + row_index * (MAP_EDITOR_UI_ICON_SIZE[1] + icon_space) if entity.item_id is not None: data = get_item_data(entity.item_id) category_name = None if data.item_equipment_category: category_name = ITEM_EQUIPMENT_CATEGORY_NAMES[data.item_equipment_category] description_lines = create_item_description(entity.item_id) item_name = build_item_name(entity.item_id) is_rare = entity.item_id.suffix_id is not None is_unique = data.is_unique tooltip = TooltipGraphics.create_for_item(self._ui_render, item_name, category_name, (x, y), description_lines, is_rare=is_rare, is_unique=is_unique) elif entity.consumable_type is not None: data = CONSUMABLES[entity.consumable_type] tooltip = TooltipGraphics.create_for_consumable(self._ui_render, data, (x, y)) elif entity.npc_type is not None: data = NON_PLAYER_CHARACTERS[entity.npc_type] tooltip = TooltipGraphics.create_for_npc(self._ui_render, entity.npc_type, data, (x, y)) elif entity.portal_id is not None: tooltip = TooltipGraphics.create_for_portal(self._ui_render, entity.portal_id, (x, y)) elif entity.is_smart_floor_tile: tooltip = TooltipGraphics.create_for_smart_floor_tile(self._ui_render, entity.entity_size, (x, y)) else: tooltip = None icon = self._create_map_editor_icon( Rect(x, y, MAP_EDITOR_UI_ICON_SIZE[0], MAP_EDITOR_UI_ICON_SIZE[1]), '', entity.sprite, None, entity.map_editor_entity_id, tooltip) self.entity_icons_by_type[entity_type].append(icon) # HANDLE USER INPUT def handle_mouse_movement(self, mouse_screen_pos: Tuple[int, int]) -> Optional[MapEditorAction]: self._set_currently_hovered_component_not_hovered() self._mouse_screen_pos = mouse_screen_pos self._entity_icon_hovered_by_mouse = None self._snapped_mouse_screen_pos = ((mouse_screen_pos[0] // self.grid_cell_size) * self.grid_cell_size, (mouse_screen_pos[1] // self.grid_cell_size) * self.grid_cell_size) self._snapped_mouse_world_pos = sum_of_vectors(self._snapped_mouse_screen_pos, self.camera_world_area.topleft) self._is_snapped_mouse_within_world = self.world_area.collidepoint(self._snapped_mouse_world_pos[0], self._snapped_mouse_world_pos[1]) self._is_snapped_mouse_over_ui = self._is_screen_position_within_ui(self._snapped_mouse_screen_pos) mouse_ui_pos = self._translate_screen_position_to_ui(mouse_screen_pos) for icon in self.entity_icons_by_type[self._shown_tab]: if icon.contains(mouse_ui_pos): self._on_hover_component(icon) entity = self._get_map_editor_entity_by_id(icon.map_editor_entity_id) self._entity_icon_hovered_by_mouse = entity return # noinspection PyTypeChecker for component in self._checkboxes + self._buttons + self._tab_buttons: if component.contains(mouse_ui_pos): self._on_hover_component(component) return if self._minimap.contains(mouse_ui_pos): self._on_hover_component(self._minimap) if self._is_left_mouse_button_down: position_ratio = self._minimap.get_position_ratio(mouse_ui_pos) return SetCameraPosition(position_ratio) return if self._is_left_mouse_button_down and self._is_snapped_mouse_within_world and not self._is_snapped_mouse_over_ui: if self._user_state.placing_entity: if self._user_state.placing_entity.wall_type or self._user_state.placing_entity.decoration_sprite: return AddEntity(self._snapped_mouse_world_pos, self._user_state.placing_entity) elif self._user_state.placing_entity.is_smart_floor_tile: return self._add_smart_floor_tiles() elif self._user_state.deleting_entities: return DeleteEntities(self._snapped_mouse_world_pos) elif self._user_state.deleting_decorations: return DeleteDecorations(self._snapped_mouse_world_pos) else: raise Exception("Unhandled user state: " + str(self._user_state)) if self._is_right_mouse_button_down and self._is_snapped_mouse_within_world and not self._is_snapped_mouse_over_ui: if self._user_state.placing_entity: if self._user_state.placing_entity.is_smart_floor_tile: return self._delete_smart_floor_tiles() def _get_map_editor_entity_by_id(self, map_editor_entity_id: int): entity = [e for e in self._entities_by_type[self._shown_tab] if e.map_editor_entity_id == map_editor_entity_id][0] return entity def _add_smart_floor_tiles(self): pos = self._snapped_mouse_world_pos tile_size = self._user_state.placing_entity.entity_size[0] for x in range(pos[0], min(pos[0] + tile_size, self.world_area.right), GRID_CELL_SIZE): for y in range(pos[1], min(pos[1] + tile_size, self.world_area.bottom), GRID_CELL_SIZE): self._smart_floor_tiles_to_add.append((x, y, GRID_CELL_SIZE, GRID_CELL_SIZE)) def _delete_smart_floor_tiles(self): pos = self._snapped_mouse_world_pos tile_size = self._user_state.placing_entity.entity_size[0] for x in range(pos[0], min(pos[0] + tile_size, self.world_area.right), GRID_CELL_SIZE): for y in range(pos[1], min(pos[1] + tile_size, self.world_area.bottom), GRID_CELL_SIZE): self._smart_floor_tiles_to_delete.append((x, y, GRID_CELL_SIZE, GRID_CELL_SIZE)) def _set_currently_hovered_component_not_hovered(self): if self._hovered_component is not None: self._hovered_component.hovered = False self._hovered_component = None def _on_hover_component(self, component): self._hovered_component = component self._hovered_component.hovered = True def handle_mouse_left_click(self) -> Optional[MapEditorAction]: if self._entity_icon_hovered_by_mouse: self._user_state = UserState.placing_entity(self._entity_icon_hovered_by_mouse) self._is_left_mouse_button_down = True # noinspection PyTypeChecker if self._hovered_component in self._checkboxes + self._buttons: return self._hovered_component.on_click() if self._hovered_component == self._minimap: mouse_ui_position = self._translate_screen_position_to_ui(self._mouse_screen_pos) position_ratio = self._minimap.get_position_ratio(mouse_ui_position) return SetCameraPosition(position_ratio) for entity_type in self._tab_buttons_by_entity_type: if self._hovered_component == self._tab_buttons_by_entity_type[entity_type]: self._set_shown_tab(entity_type) if self._user_state.placing_entity: entity_being_placed = self._user_state.placing_entity if self._is_snapped_mouse_within_world and not self._is_snapped_mouse_over_ui: if entity_being_placed.is_smart_floor_tile: return self._add_smart_floor_tiles() else: return AddEntity(self._snapped_mouse_world_pos, entity_being_placed) elif self._user_state.deleting_entities: return DeleteEntities(self._snapped_mouse_world_pos) elif self._user_state.deleting_decorations: return DeleteDecorations(self._snapped_mouse_world_pos) else: raise Exception("Unhandled user state: %s" % self._user_state) def handle_mouse_right_click(self) -> Optional[MapEditorAction]: self._is_right_mouse_button_down = True if self._user_state.placing_entity: entity_being_placed = self._user_state.placing_entity if self._is_snapped_mouse_within_world and not self._is_snapped_mouse_over_ui: if entity_being_placed.is_smart_floor_tile: return self._delete_smart_floor_tiles() def handle_mouse_left_release(self): self._is_left_mouse_button_down = False if self._is_snapped_mouse_within_world and not self._is_snapped_mouse_over_ui: entity = self._user_state.placing_entity if entity and entity.is_smart_floor_tile: tiles = list(self._smart_floor_tiles_to_add) self._smart_floor_tiles_to_add.clear() return AddSmartFloorTiles(tiles) def handle_mouse_right_release(self): self._is_right_mouse_button_down = False if self._is_snapped_mouse_within_world and not self._is_snapped_mouse_over_ui: entity = self._user_state.placing_entity if entity and entity.is_smart_floor_tile: tiles = list(self._smart_floor_tiles_to_delete) self._smart_floor_tiles_to_delete.clear() return DeleteSmartFloorTiles(tiles) def handle_key_down(self, key): number_keys = [pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, pygame.K_5, pygame.K_6, pygame.K_7, pygame.K_8, pygame.K_9, pygame.K_0] if key == pygame.K_q: self._user_state = UserState.deleting_entities() elif key == pygame.K_z: self._user_state = UserState.deleting_decorations() elif key in self._tabs_by_pygame_key: self._set_shown_tab(self._tabs_by_pygame_key[key]) elif key in number_keys: index = number_keys.index(key) shown_entity_icons = self.entity_icons_by_type[self._shown_tab] if index < len(shown_entity_icons): targeted_icon = shown_entity_icons[index] entity = self._get_map_editor_entity_by_id(targeted_icon.map_editor_entity_id) self._user_state = UserState.placing_entity(entity) def _translate_ui_position_to_screen(self, position): return position[0] + self._ui_screen_area.x, position[1] + self._ui_screen_area.y def _translate_screen_position_to_ui(self, position: Tuple[int, int]): return position[0] - self._ui_screen_area.x, position[1] - self._ui_screen_area.y def _get_image_for_sprite(self, sprite: Sprite, direction: Direction, animation_progress: float) -> ImageWithRelativePosition: images: Dict[Direction, List[ImageWithRelativePosition]] = self._images_by_sprite[sprite] if direction in images: images_for_this_direction = images[direction] else: images_for_this_direction = next(iter(images.values())) animation_frame_index = int(len(images_for_this_direction) * animation_progress) return images_for_this_direction[animation_frame_index] def _set_shown_tab(self, shown_tab: EntityTab): if self._shown_tab: self._tab_buttons_by_entity_type[self._shown_tab].enabled = False self._shown_tab = shown_tab self._tab_buttons_by_entity_type[shown_tab].enabled = True def render( self, num_enemies: int, num_walls: int, num_decorations: int, npc_positions: List[Tuple[int, int]], player_position: Tuple[int, int], grid: Grid, named_world_positions: Dict[Tuple[int, int], str], fps_string: str): x_camera = self.camera_world_area.x y_camera = self.camera_world_area.y placing_entity = self._user_state.placing_entity if placing_entity and placing_entity.is_smart_floor_tile: xmin = (x_camera - self.world_area.x) // GRID_CELL_SIZE xmax = xmin + self.camera_world_area.w // GRID_CELL_SIZE ymin = (y_camera - self.world_area.y) // GRID_CELL_SIZE ymax = ymin + self.camera_world_area.h // GRID_CELL_SIZE for x in range(xmin, xmax): for y in range(ymin, ymax): if grid.is_floor((x, y)): rect = Rect(x * GRID_CELL_SIZE + self.world_area.x - x_camera, y * GRID_CELL_SIZE + self.world_area.y - y_camera, GRID_CELL_SIZE, GRID_CELL_SIZE) self._screen_render.rect((100, 180, 250), rect, 2) moused_over_names = [s for (p, s) in named_world_positions.items() if p == self._snapped_mouse_world_pos] self._screen_render.rect(COLOR_BLACK, Rect(0, 0, self._camera_size[0], self._camera_size[1]), 3) self._screen_render.rect_filled(COLOR_BLACK, Rect(0, self._camera_size[1], self._screen_size[0], self._screen_size[1] - self._camera_size[1])) self.button_delete_entities.render(self._user_state.deleting_entities) self.button_delete_decorations.render(self._user_state.deleting_decorations) for icon in self.entity_icons_by_type[self._shown_tab]: highlighted = placing_entity and placing_entity.map_editor_entity_id == icon.map_editor_entity_id icon.render(highlighted) self._screen_render.rect(COLOR_WHITE, self._ui_screen_area, 1) debug_entries = [ self._map_file_path, "--------------------------------", fps_string + " fps", "# enemies: " + str(num_enemies), "# walls: " + str(num_walls), "# decorations: " + str(num_decorations), "Cell size: " + str(self.grid_cell_size), "Mouse world pos: " + str(self._snapped_mouse_world_pos), "Mouse over: " + (moused_over_names[0] if moused_over_names else "...") ] self._screen_render.rect_transparent(Rect(0, 0, 280, 5 + len(debug_entries) * 22), 150, COLOR_BLACK) for i, entry in enumerate(debug_entries): self._screen_render.text(self._font_debug_info, entry, (10, 12 + i * 20)) self._minimap.update_camera_area(self.camera_world_area) self._minimap.update_npc_positions(npc_positions) self._minimap.update_player_position(player_position) self._minimap.update_world_area(self.world_area) # noinspection PyTypeChecker for component in self._checkboxes + self._tab_buttons + self._buttons + [self._minimap]: component.render() if self._hovered_component and self._hovered_component.tooltip: tooltip: TooltipGraphics = self._hovered_component.tooltip tooltip.render() if not self._is_snapped_mouse_over_ui: snapped_mouse_rect = Rect(self._snapped_mouse_screen_pos[0], self._snapped_mouse_screen_pos[1], self.grid_cell_size, self.grid_cell_size) if not self._is_snapped_mouse_within_world: self._screen_render.rect((250, 50, 0), snapped_mouse_rect, 3) elif placing_entity: if not placing_entity.is_smart_floor_tile: self._render_map_editor_world_entity_at_position( placing_entity.sprite, self._snapped_mouse_screen_pos) self._render_placed_entity_outline(self._snapped_mouse_screen_pos, placing_entity.entity_size) elif self._user_state.deleting_entities: self._screen_render.rect((250, 250, 0), snapped_mouse_rect, 3) elif self._user_state.deleting_decorations: self._screen_render.rect((0, 250, 250), snapped_mouse_rect, 3) else: raise Exception("Unhandled user_state: " + str(self._user_state)) for tile in self._smart_floor_tiles_to_add: rect = Rect(tile[0] - x_camera, tile[1] - y_camera, tile[2], tile[3]) self._screen_render.rect((180, 180, 250), rect, 2) for tile in self._smart_floor_tiles_to_delete: rect = Rect(tile[0] - x_camera, tile[1] - y_camera, tile[2], tile[3]) self._screen_render.rect((250, 180, 180), rect, 2) def update_wall_positions(self, wall_positions): self._minimap.set_walls(wall_positions) def _render_map_editor_world_entity_at_position(self, sprite: Sprite, position: Tuple[int, int]): image_with_relative_position = self._get_image_for_sprite(sprite, Direction.DOWN, 0) sprite_position = sum_of_vectors(position, image_with_relative_position.position_relative_to_entity) self._screen_render.image(image_with_relative_position.image, sprite_position) def _render_placed_entity_outline(self, position: Tuple[int, int], entity_size: Tuple[int, int]): self._screen_render.rect((50, 250, 0), Rect(position[0], position[1], entity_size[0], entity_size[1]), 3) def _create_map_editor_icon(self, rect: Rect, user_input_key: str, sprite: Optional[Sprite], ui_icon_sprite: Optional[UiIconSprite], map_editor_entity_id: int, tooltip: Optional[TooltipGraphics]) -> MapEditorIcon: if sprite: image = self._images_by_sprite[sprite][Direction.DOWN][0].image elif ui_icon_sprite: image = self._images_by_ui_sprite[ui_icon_sprite] else: raise Exception("Nothing to render!") return MapEditorIcon(self._ui_render, rect, image, self._font_ui_icon_keys, user_input_key, map_editor_entity_id, tooltip) def _is_screen_position_within_ui(self, screen_position: Tuple[int, int]): ui_position = self._translate_screen_position_to_ui(screen_position) return ui_position[1] >= 0
class GameUiView: def __init__(self, pygame_screen, camera_size: Tuple[int, int], screen_size: Tuple[int, int], images_by_ui_sprite: Dict[UiIconSprite, Any], big_images_by_ui_sprite: Dict[UiIconSprite, Any], images_by_portrait_sprite: Dict[PortraitIconSprite, Any], ability_key_labels: List[str]): # INIT PYGAME FONTS pygame.font.init() # SETUP FUNDAMENTALS self.screen_render = DrawableArea(pygame_screen) self.ui_render = DrawableArea(pygame_screen, self._translate_ui_position_to_screen) self.ui_screen_area = Rect(0, camera_size[1], screen_size[0], screen_size[1] - camera_size[1]) self.camera_size = camera_size self.screen_size = screen_size self.ability_key_labels = ability_key_labels # FONTS self.font_splash_screen = pygame.font.Font( DIR_FONTS + 'Arial Rounded Bold.ttf', 64) self.font_ui_stat_bar_numbers = pygame.font.Font( DIR_FONTS + 'Monaco.dfont', 12) self.font_ui_money = pygame.font.Font(DIR_FONTS + 'Monaco.dfont', 12) self.font_tooltip_details = pygame.font.Font( DIR_FONTS + 'Monaco.dfont', 12) self.font_buttons = pygame.font.Font(DIR_FONTS + 'Monaco.dfont', 12) self.font_stats = pygame.font.Font(DIR_FONTS + 'Monaco.dfont', 9) self.font_buff_texts = pygame.font.Font(DIR_FONTS + 'Monaco.dfont', 12) self.font_message = pygame.font.Font(DIR_FONTS + 'Monaco.dfont', 14) self.font_debug_info = pygame.font.Font(DIR_FONTS + 'Monaco.dfont', 12) self.font_ui_icon_keys = pygame.font.Font( DIR_FONTS + 'Courier New Bold.ttf', 12) self.font_level = pygame.font.Font(DIR_FONTS + 'Courier New Bold.ttf', 11) # IMAGES self.images_by_ui_sprite = images_by_ui_sprite self.big_images_by_ui_sprite = big_images_by_ui_sprite self.images_by_portrait_sprite = images_by_portrait_sprite self.images_by_item_category = { ItemEquipmentCategory.HEAD: self.images_by_ui_sprite[UiIconSprite.INVENTORY_TEMPLATE_HELMET], ItemEquipmentCategory.CHEST: self.images_by_ui_sprite[UiIconSprite.INVENTORY_TEMPLATE_CHEST], ItemEquipmentCategory.MAIN_HAND: self.images_by_ui_sprite[UiIconSprite.INVENTORY_TEMPLATE_MAINHAND], ItemEquipmentCategory.OFF_HAND: self.images_by_ui_sprite[UiIconSprite.INVENTORY_TEMPLATE_OFFHAND], ItemEquipmentCategory.NECK: self.images_by_ui_sprite[UiIconSprite.INVENTORY_TEMPLATE_NECK], ItemEquipmentCategory.RING: self.images_by_ui_sprite[UiIconSprite.INVENTORY_TEMPLATE_RING], } # UI COMPONENTS self.ability_icons_row: Rect = Rect(0, 0, 0, 0) self.ability_icons: List[AbilityIcon] = [] self.consumable_icons_row: Rect = Rect(0, 0, 0, 0) self.consumable_icons: List[ConsumableIcon] = [] self.inventory_icons_rect: Rect = Rect(0, 0, 0, 0) self.inventory_icons: List[ItemIcon] = [] self.exp_bar = ExpBar(self.ui_render, Rect(135, 8, 300, 2), self.font_level) self.minimap = Minimap(self.ui_render, Rect(475, 52, 80, 80), Rect(0, 0, 1, 1), (0, 0)) self.buffs = Buffs(self.ui_render, self.font_buff_texts, (10, -35)) self.money_text = Text(self.ui_render, self.font_ui_money, (24, 150), "NO MONEY") self.talents_window: TalentsWindow = None self.quests_window: QuestsWindow = None self.message = Message(self.screen_render, self.font_message, self.ui_screen_area.w // 2, self.ui_screen_area.y - 30) self.paused_splash_screen = PausedSplashScreen( self.screen_render, self.font_splash_screen, Rect(0, 0, self.screen_size[0], self.screen_size[1])) self.controls_window = ControlsWindow(self.ui_render, self.font_tooltip_details, self.font_stats) # SETUP UI COMPONENTS self._setup_ability_icons() self._setup_consumable_icons() self._setup_inventory_icons() self._setup_health_and_mana_bars() self._setup_stats_window() self._setup_talents_window(TalentsState(TalentsConfig({}))) self._setup_quests_window() self._setup_toggle_buttons() self._setup_portrait() self._setup_dialog() # QUICKLY CHANGING STATE self.hovered_component = None self.fps_string = "" self.game_mode_string = "" self.enabled_toggle: ToggleButton = None self.item_slot_being_dragged: ItemIcon = None self.consumable_slot_being_dragged: ConsumableIcon = None self.is_mouse_hovering_ui = False self.mouse_screen_position = (0, 0) self.dialog_state = DialogState() self._ticks_since_last_consumable_action = 0 self._ticks_since_last_ability_action = 0 self.highlighted_consumable_action: Optional[int] = None self.highlighted_ability_action: Optional[AbilityType] = None self.manually_highlighted_inventory_item: Optional[ ItemId] = None # used for dialog self.info_message = InfoMessage() def _setup_ability_icons(self): x_0 = 140 y = 112 icon_space = 2 icon_rect_padding = 2 abilities_rect_pos = (x_0 - icon_rect_padding, y - icon_rect_padding) max_num_abilities = 5 self.ability_icons_row = Rect( abilities_rect_pos[0], abilities_rect_pos[1], (UI_ICON_SIZE[0] + icon_space) * max_num_abilities - icon_space + icon_rect_padding * 2, UI_ICON_SIZE[1] + icon_rect_padding * 2) self.ability_icons = [] for i in range(max_num_abilities): x = x_0 + i * (UI_ICON_SIZE[0] + icon_space) rect = Rect(x, y, UI_ICON_SIZE[0], UI_ICON_SIZE[1]) icon = AbilityIcon(self.ui_render, rect, None, None, self.font_ui_icon_keys, None, None, 0) self.ability_icons.append(icon) def _setup_consumable_icons(self): x_0 = 140 y = 52 icon_space = 2 icon_rect_padding = 2 consumables_rect_pos = (x_0 - icon_rect_padding, y - icon_rect_padding) max_num_consumables = 5 self.consumable_icons_row = Rect( consumables_rect_pos[0], consumables_rect_pos[1], (UI_ICON_SIZE[0] + icon_space) * max_num_consumables - icon_space + icon_rect_padding * 2, UI_ICON_SIZE[1] + icon_rect_padding * 2) self.consumable_icons = [] for i in range(max_num_consumables): x = x_0 + i * (UI_ICON_SIZE[0] + icon_space) rect = Rect(x, y, UI_ICON_SIZE[0], UI_ICON_SIZE[1]) slot_number = i + 1 icon = ConsumableIcon(self.ui_render, rect, None, str(slot_number), self.font_ui_icon_keys, None, [], slot_number) self.consumable_icons.append(icon) def _setup_inventory_icons(self): x_0 = 325 y_0 = 24 icon_space = 2 icon_rect_padding = 2 items_rect_pos = (x_0 - icon_rect_padding, y_0 - icon_rect_padding) num_item_slot_rows = 4 num_slots_per_row = 3 self.inventory_icons_rect = Rect( items_rect_pos[0], items_rect_pos[1], (UI_ICON_SIZE[0] + icon_space) * num_slots_per_row - icon_space + icon_rect_padding * 2, num_item_slot_rows * UI_ICON_SIZE[1] + (num_item_slot_rows - 1) * icon_space + icon_rect_padding * 2) for i in range(num_item_slot_rows * num_slots_per_row): x = x_0 + (i % num_slots_per_row) * (UI_ICON_SIZE[0] + icon_space) y = y_0 + (i // num_slots_per_row) * (UI_ICON_SIZE[1] + icon_space) rect = Rect(x, y, UI_ICON_SIZE[0], UI_ICON_SIZE[1]) icon = ItemIcon(self.ui_render, rect, None, None, None, None, i) self.inventory_icons.append(icon) def _setup_health_and_mana_bars(self): rect_healthbar = Rect(20, 111, 100, 14) self.healthbar = StatBar(self.ui_render, rect_healthbar, (200, 0, 50), None, 0, 1, show_numbers=True, font=self.font_ui_stat_bar_numbers) rect_manabar = Rect(20, 132, 100, 14) self.manabar = StatBar(self.ui_render, rect_manabar, (50, 0, 200), None, 0, 1, show_numbers=True, font=self.font_ui_stat_bar_numbers) def _setup_toggle_buttons(self): x = 600 y_0 = 12 w = 150 h = 20 font = self.font_buttons self.stats_toggle = ToggleButton(self.ui_render, Rect(x, y_0, w, h), font, "STATS [A]", ToggleButtonId.STATS, False, self.stats_window) self.talents_toggle = ToggleButton(self.ui_render, Rect(x, y_0 + 25, w, h), font, "TALENTS [N]", ToggleButtonId.TALENTS, False, self.talents_window) # TODO Add hotkey, and handle that user input in this module self.quests_toggle = ToggleButton(self.ui_render, Rect(x, y_0 + 50, w, h), font, "QUESTS [B]", ToggleButtonId.QUESTS, False, self.quests_window) self.controls_toggle = ToggleButton(self.ui_render, Rect(x, y_0 + 75, w, h), font, "HELP [H]", ToggleButtonId.HELP, False, self.controls_window) self.toggle_buttons = [ self.stats_toggle, self.talents_toggle, self.quests_toggle, self.controls_toggle ] self.sound_checkbox = Checkbox(self.ui_render, Rect(x, y_0 + 100, 70, h), "SOUND", True, lambda _: ToggleSound()) self.save_button = Button(self.ui_render, Rect(x + 80, y_0 + 100, 70, h), "SAVE [S]", lambda: SaveGame()) self.fullscreen_checkbox = Checkbox(self.ui_render, Rect(x, y_0 + 125, w, h), "FULLSCREEN", True, lambda _: ToggleFullscreen()) def _setup_stats_window(self): self.stats_window = StatsWindow(self.ui_render, self.font_tooltip_details, self.font_stats, None, 0, None, 1) def _setup_quests_window(self): self.quests_window = QuestsWindow(self.ui_render, self.font_tooltip_details, self.font_stats) def _setup_talents_window(self, talents: TalentsState): talent_tiers: List[TalentTierData] = [] option_data_from_config = lambda config: TalentOptionData( config.name, config.description, self.images_by_ui_sprite[ config.ui_icon_sprite]) for tier_state in talents.tiers: options = [ option_data_from_config(config) for config in tier_state.options ] tier_data = TalentTierData(tier_state.status, tier_state.required_level, tier_state.picked_index, options) talent_tiers.append(tier_data) if self.talents_window is None: self.talents_window = TalentsWindow(self.ui_render, self.font_tooltip_details, self.font_stats, talent_tiers) else: self.talents_window.update(talent_tiers) def _setup_portrait(self): rect = Rect(20, 18, PORTRAIT_ICON_SIZE[0], PORTRAIT_ICON_SIZE[1]) self.portrait = Portrait(self.ui_render, rect, None) def _setup_dialog(self): self.dialog = Dialog(self.screen_render, None, None, [], 0, PORTRAIT_ICON_SIZE, UI_ICON_SIZE) # -------------------------------------------------------------------------------------------------------- # HANDLE USER INPUT # -------------------------------------------------------------------------------------------------------- def handle_mouse_movement(self, mouse_screen_pos: Tuple[int, int]): self.mouse_screen_position = mouse_screen_pos self.is_mouse_hovering_ui = is_point_in_rect(mouse_screen_pos, self.ui_screen_area) self._check_for_hovered_components() def handle_mouse_movement_in_dialog(self, mouse_screen_pos: Tuple[int, int]): self.mouse_screen_position = mouse_screen_pos self.is_mouse_hovering_ui = is_point_in_rect(mouse_screen_pos, self.ui_screen_area) self.dialog.handle_mouse_movement(mouse_screen_pos) def handle_mouse_click_in_dialog( self) -> Optional[Tuple[NpcType, int, int]]: clicked_option_index = self.dialog.get_hovered_option_index() if clicked_option_index is not None: previous_index = self.dialog_state.option_index self.dialog_state.option_index = clicked_option_index self._update_dialog_graphics() return self.dialog_state.npc.npc_type, previous_index, clicked_option_index def _check_for_hovered_components(self): mouse_ui_position = self._translate_screen_position_to_ui( self.mouse_screen_position) # noinspection PyTypeChecker simple_components = [ self.healthbar, self.manabar, self.sound_checkbox, self.save_button, self.fullscreen_checkbox ] + self.ability_icons + self.toggle_buttons for component in simple_components: if component.contains(mouse_ui_position): self._on_hover_component(component) return # TODO Unify hover handling for consumables/items # noinspection PyTypeChecker for icon in self.consumable_icons + self.inventory_icons: collision_offset = icon.get_collision_offset(mouse_ui_position) if collision_offset: self._on_hover_component(icon) return # TODO Unify hover handling of window icons if self.talents_window.shown: hovered_icon = self.talents_window.get_icon_containing( mouse_ui_position) if hovered_icon: self._on_hover_component(hovered_icon) return # If something was hovered, we would have returned from the method self._set_currently_hovered_component_not_hovered() def _on_hover_component(self, component): self._set_currently_hovered_component_not_hovered() self.hovered_component = component self.hovered_component.hovered = True def _set_currently_hovered_component_not_hovered(self): if self.hovered_component is not None: self.hovered_component.hovered = False self.hovered_component = None def handle_mouse_click(self) -> List[EventTriggeredFromUi]: if self.hovered_component in self.toggle_buttons: self._on_click_toggle(self.hovered_component) elif self.hovered_component in [ self.save_button, self.fullscreen_checkbox, self.sound_checkbox ]: return [self.hovered_component.on_click()] elif self.hovered_component in self.inventory_icons and self.hovered_component.item_id: self.item_slot_being_dragged = self.hovered_component return [StartDraggingItemOrConsumable()] elif self.hovered_component in self.consumable_icons and self.hovered_component.consumable_types: self.consumable_slot_being_dragged = self.hovered_component return [StartDraggingItemOrConsumable()] elif self.hovered_component in self.talents_window.get_pickable_talent_icons( ): talent_icon: TalentIcon = self.hovered_component return [ PickTalent(talent_icon.tier_index, talent_icon.option_index) ] return [] def handle_mouse_right_click(self) -> List[EventTriggeredFromUi]: if self.hovered_component in self.inventory_icons: self.item_slot_being_dragged = None item_icon: ItemIcon = self.hovered_component return [TrySwitchItemInInventory(item_icon.inventory_slot_index)] return [] def handle_mouse_release(self) -> List[EventTriggeredFromUi]: triggered_events = [] if self.item_slot_being_dragged: if self.hovered_component in self.inventory_icons and self.hovered_component != self.item_slot_being_dragged: item_icon: ItemIcon = self.hovered_component event = DragItemBetweenInventorySlots( self.item_slot_being_dragged.inventory_slot_index, item_icon.inventory_slot_index) triggered_events.append(event) if not self.is_mouse_hovering_ui: event = DropItemOnGround( self.item_slot_being_dragged.inventory_slot_index, self.mouse_screen_position) triggered_events.append(event) self.item_slot_being_dragged = None if self.consumable_slot_being_dragged: if self.hovered_component in self.consumable_icons and self.hovered_component != self.consumable_slot_being_dragged: consumable_icon: ConsumableIcon = self.hovered_component event = DragConsumableBetweenInventorySlots( self.consumable_slot_being_dragged.slot_number, consumable_icon.slot_number) triggered_events.append(event) if not self.is_mouse_hovering_ui: event = DropConsumableOnGround( self.consumable_slot_being_dragged.slot_number, self.mouse_screen_position) triggered_events.append(event) self.consumable_slot_being_dragged = None return triggered_events def handle_space_click(self) -> Optional[Tuple[NonPlayerCharacter, int]]: if self.dialog_state.active: self.dialog_state.active = False self._update_dialog_graphics() return self.dialog_state.npc, self.dialog_state.option_index return None def handle_key_press(self, key) -> List[EventTriggeredFromUi]: if key == pygame.K_s: return [SaveGame()] elif key == pygame.K_n: self._click_toggle_button(ToggleButtonId.TALENTS) return [ToggleWindow()] elif key == pygame.K_a: self._click_toggle_button(ToggleButtonId.STATS) return [ToggleWindow()] elif key == pygame.K_h: self._click_toggle_button(ToggleButtonId.HELP) return [ToggleWindow()] elif key == pygame.K_b: self._click_toggle_button(ToggleButtonId.QUESTS) return [ToggleWindow()] return [] # -------------------------------------------------------------------------------------------------------- # HANDLE TOGGLE BUTTON USER INTERACTIONS # -------------------------------------------------------------------------------------------------------- def _click_toggle_button(self, ui_toggle: ToggleButtonId): toggle = [ tb for tb in self.toggle_buttons if tb.toggle_id == ui_toggle ][0] self._on_click_toggle(toggle) def _on_click_toggle(self, clicked_toggle: ToggleButton): play_sound(SoundId.UI_TOGGLE) if clicked_toggle.is_open: self.enabled_toggle.close() self.enabled_toggle = None else: if self.enabled_toggle is not None: self.enabled_toggle.close() self.enabled_toggle = clicked_toggle self.enabled_toggle.open() self._check_for_hovered_components() def close_talent_window(self): if self.enabled_toggle == self.talents_toggle: self.enabled_toggle.close() self.enabled_toggle = None self._check_for_hovered_components() # -------------------------------------------------------------------------------------------------------- # REACT TO OBSERVABLE EVENTS # -------------------------------------------------------------------------------------------------------- def on_talents_updated(self, talents_state: TalentsState): self._setup_talents_window(talents_state) if talents_state.has_unpicked_talents( ) and not self.talents_toggle.is_open: self.talents_toggle.highlighted = True def on_talent_was_unlocked(self, _event): if self.enabled_toggle != self.talents_toggle: self.talents_toggle.highlighted = True def on_ability_was_clicked(self, ability_type: AbilityType): self.highlighted_ability_action = ability_type self._ticks_since_last_ability_action = 0 def on_consumable_was_clicked(self, slot_number: int): self.highlighted_consumable_action = slot_number self._ticks_since_last_consumable_action = 0 def on_player_movement_speed_updated(self, speed_multiplier: float): self.stats_window.player_speed_multiplier = speed_multiplier def on_player_exp_updated(self, event: Tuple[int, float]): level, ratio_exp_until_next_level = event self.exp_bar.update(level, ratio_exp_until_next_level) self.stats_window.level = level def on_player_quests_updated(self, event: Tuple[List[Quest], List[Quest]]): active_quests, completed_quests = event self.quests_window.active_quests = list(active_quests) self.quests_window.completed_quests = list(completed_quests) def on_money_updated(self, money: int): self.money_text.text = "Money: " + str(money) def on_cooldowns_updated(self, ability_cooldowns_remaining: Dict[AbilityType, int]): for icon in self.ability_icons: ability_type = icon.ability_type if ability_type: ability = ABILITIES[ability_type] icon.cooldown_remaining_ratio = ability_cooldowns_remaining[ ability_type] / ability.cooldown def on_health_updated(self, health: Tuple[int, int]): value, max_value = health self.healthbar.update(value, max_value) def on_mana_updated(self, mana: Tuple[int, int]): value, max_value = mana self.manabar.update(value, max_value) def on_buffs_updated(self, active_buffs: List[BuffWithDuration]): buffs = [] for active_buff in active_buffs: buff_type = active_buff.buff_effect.get_buff_type() # Buffs that don't have description texts shouldn't be displayed. (They are typically irrelevant to the # player) if buff_type in BUFF_TEXTS: text = BUFF_TEXTS[buff_type] ratio_remaining = active_buff.get_ratio_duration_remaining() buffs.append((text, ratio_remaining)) self.buffs.update(buffs) def on_player_stats_updated(self, player_state: PlayerState): self.stats_window.player_state = player_state health_regen = player_state.health_resource.get_effective_regen() mana_regen = player_state.mana_resource.get_effective_regen() tooltip_details = [ DetailLine("regeneration: " + "{:.1f}".format(health_regen) + "/s") ] health_tooltip = TooltipGraphics( self.ui_render, COLOR_WHITE, "Health", tooltip_details, bottom_left=(self.healthbar.rect.left - 2, self.healthbar.rect.top - 1)) self.healthbar.tooltip = health_tooltip tooltip_details = [ DetailLine("regeneration: " + "{:.1f}".format(mana_regen) + "/s") ] mana_tooltip = TooltipGraphics(self.ui_render, COLOR_WHITE, "Mana", tooltip_details, bottom_left=(self.manabar.rect.left - 2, self.manabar.rect.top - 1)) self.manabar.tooltip = mana_tooltip def on_abilities_updated(self, abilities_by_key_string: Dict[str, AbilityType]): for i, icon in enumerate(self.ability_icons): key_string = self.ability_key_labels[i] ability_type = abilities_by_key_string.get(key_string, None) ability = ABILITIES[ability_type] if ability_type else None image = self.images_by_ui_sprite[ ability.icon_sprite] if ability else None icon.update(image=image, label=key_string, ability=ability, ability_type=ability_type) def on_consumables_updated(self, consumable_slots: Dict[int, List[ConsumableType]]): for i, slot_number in enumerate(consumable_slots): icon = self.consumable_icons[i] consumable_types = consumable_slots[slot_number] image = None consumable = None if consumable_types: consumable = CONSUMABLES[consumable_types[0]] image = self.images_by_ui_sprite[consumable.icon_sprite] icon.update(image, consumable, consumable_types) def on_inventory_updated(self, item_slots: List[ItemInventorySlot]): for i in range(len(item_slots)): icon = self.inventory_icons[i] slot = item_slots[i] item_id = slot.get_item_id() if not slot.is_empty() else None slot_equipment_category = slot.enforced_equipment_category image = None tooltip = None if item_id: item_type = item_id.item_type data = get_item_data_by_type(item_type) image = self.images_by_ui_sprite[data.icon_sprite] category_name = None if data.item_equipment_category: category_name = ITEM_EQUIPMENT_CATEGORY_NAMES[ data.item_equipment_category] description_lines = create_item_description(item_id) item_name = item_id.name is_rare = bool(item_id.affix_stats) is_unique = data.is_unique tooltip = TooltipGraphics.create_for_item(self.ui_render, item_name, category_name, icon.rect.topleft, description_lines, is_rare=is_rare, is_unique=is_unique) elif slot_equipment_category: image = self.images_by_item_category[slot_equipment_category] category_name = ITEM_EQUIPMENT_CATEGORY_NAMES[ slot_equipment_category] tooltip_details = [ DetailLine("[" + category_name + "]"), DetailLine( "You have nothing equipped. Drag an item here to equip it!" ) ] tooltip = TooltipGraphics(self.ui_render, COLOR_WHITE, "...", tooltip_details, bottom_left=icon.rect.topleft) icon.image = image icon.tooltip = tooltip icon.slot_equipment_category = slot_equipment_category icon.item_id = item_id def on_fullscreen_changed(self, fullscreen: bool): self.fullscreen_checkbox.checked = fullscreen def on_world_area_updated(self, world_area: Rect): self.minimap.update_world_area(world_area) def on_walls_seen(self, seen_wall_positions: List[Tuple[int, int]]): self.minimap.add_walls(seen_wall_positions) def on_player_position_updated(self, center_position: Tuple[int, int]): self.minimap.update_player_position(center_position) # -------------------------------------------------------------------------------------------------------- # HANDLE DIALOG USER INTERACTIONS # -------------------------------------------------------------------------------------------------------- def start_dialog_with_npc(self, npc: NonPlayerCharacter, dialog_data: DialogData) -> int: self.dialog_state.active = True self.dialog_state.npc = npc self.dialog_state.data = dialog_data if self.dialog_state.option_index >= len(dialog_data.options): self.dialog_state.option_index = 0 self._update_dialog_graphics() return self.dialog_state.option_index def change_dialog_option(self, delta: int) -> Tuple[NpcType, int, int]: previous_option_index = self.dialog_state.option_index self.dialog_state.option_index = (self.dialog_state.option_index + delta) % len( self.dialog_state.data.options) self._update_dialog_graphics() new_option_index = self.dialog_state.option_index return self.dialog_state.npc.npc_type, previous_option_index, new_option_index def _update_dialog_graphics(self): if self.dialog_state.active: build_dialog_option = lambda option: DialogOption( option.summary, option.action_text, self.images_by_ui_sprite[ option.ui_icon_sprite] if option.ui_icon_sprite else None, option.detail_header, option.detail_body) data = self.dialog_state.data image = self.images_by_portrait_sprite[data.portrait_icon_sprite] text_body = data.text_body options = [build_dialog_option(option) for option in data.options] active_option_index = self.dialog_state.option_index self.dialog.show_with_data(data.name, image, text_body, options, active_option_index) else: self.dialog.hide() def has_open_dialog(self) -> bool: return self.dialog_state.active # -------------------------------------------------------------------------------------------------------- # UPDATE MISC. STATE # -------------------------------------------------------------------------------------------------------- def update(self, time_passed: Millis): self.minimap.update(time_passed) self._ticks_since_last_consumable_action += time_passed if self._ticks_since_last_consumable_action > HIGHLIGHT_CONSUMABLE_ACTION_DURATION: self.highlighted_consumable_action = None self._ticks_since_last_ability_action += time_passed if self._ticks_since_last_ability_action > HIGHLIGHT_ABILITY_ACTION_DURATION: self.highlighted_ability_action = None self.info_message.notify_time_passed(time_passed) def update_hero(self, hero_id: HeroId): sprite = HEROES[hero_id].portrait_icon_sprite image = self.images_by_portrait_sprite[sprite] self.portrait.image = image self.stats_window.hero_id = hero_id def set_paused(self, paused: bool): self.paused_splash_screen.shown = paused def update_fps_string(self, fps_string: str): self.fps_string = fps_string def update_game_mode_string(self, game_mode_string: str): self.game_mode_string = game_mode_string def remove_highlight_from_talent_toggle(self): self.talents_toggle.highlighted = False def set_minimap_highlight(self, position_ratio: Tuple[float, float]): self.minimap.set_highlight(position_ratio) def remove_minimap_highlight(self): self.minimap.remove_highlight() def set_inventory_highlight(self, item_id: ItemId): for icon in self.inventory_icons: if icon.item_id == item_id: self.manually_highlighted_inventory_item = item_id def remove_inventory_highlight(self): self.manually_highlighted_inventory_item = None # -------------------------------------------------------------------------------------------------------- # RENDERING # -------------------------------------------------------------------------------------------------------- def _translate_ui_position_to_screen(self, position): return position[0] + self.ui_screen_area.x, position[ 1] + self.ui_screen_area.y def _translate_screen_position_to_ui(self, position: Tuple[int, int]): return position[0] - self.ui_screen_area.x, position[ 1] - self.ui_screen_area.y def _render_item_being_dragged(self, item_id: ItemId, mouse_screen_position: Tuple[int, int], relative_mouse_pos: Tuple[int, int]): ui_icon_sprite = get_item_data(item_id).icon_sprite big_image = self.big_images_by_ui_sprite[ui_icon_sprite] self._render_dragged(big_image, mouse_screen_position, relative_mouse_pos) def _render_consumable_being_dragged(self, consumable_type: ConsumableType, mouse_screen_position: Tuple[int, int], relative_mouse_pos: Tuple[int, int]): ui_icon_sprite = CONSUMABLES[consumable_type].icon_sprite big_image = self.big_images_by_ui_sprite[ui_icon_sprite] self._render_dragged(big_image, mouse_screen_position, relative_mouse_pos) def _render_dragged(self, big_image, mouse_screen_position, relative_mouse_pos): position = (mouse_screen_position[0] - relative_mouse_pos[0] - (UI_ICON_BIG_SIZE[0] - UI_ICON_SIZE[0]) // 2, mouse_screen_position[1] - relative_mouse_pos[1] - (UI_ICON_BIG_SIZE[1] - UI_ICON_SIZE[1]) // 2) self.screen_render.image(big_image, position) def render(self): self.screen_render.rect( COLOR_BORDER, Rect(0, 0, self.camera_size[0], self.camera_size[1]), 1) self.screen_render.rect_filled( (20, 10, 0), Rect(0, self.camera_size[1], self.screen_size[0], self.screen_size[1] - self.camera_size[1])) # CONSUMABLES self.ui_render.rect_filled((60, 60, 80), self.consumable_icons_row) for icon in self.consumable_icons: # TODO treat this as state and update it elsewhere recently_clicked = icon.slot_number == self.highlighted_consumable_action icon.render(recently_clicked) # ABILITIES self.ui_render.rect_filled((60, 60, 80), self.ability_icons_row) for icon in self.ability_icons: ability_type = icon.ability_type # TODO treat this as state and update it elsewhere if ability_type: recently_clicked = ability_type == self.highlighted_ability_action icon.render(recently_clicked) # ITEMS self.ui_render.rect_filled((60, 60, 80), self.inventory_icons_rect) for icon in self.inventory_icons: # TODO treat this as state and update it elsewhere highlighted = False if self.item_slot_being_dragged and self.item_slot_being_dragged.item_id: item_type = self.item_slot_being_dragged.item_id.item_type item_data = get_item_data_by_type(item_type) highlighted = item_data.item_equipment_category \ and icon.slot_equipment_category == item_data.item_equipment_category if self.manually_highlighted_inventory_item and self.manually_highlighted_inventory_item == icon.item_id: highlighted = True icon.render(highlighted) # MINIMAP self.minimap.render() simple_components = [ self.exp_bar, self.portrait, self.healthbar, self.manabar, self.money_text, self.buffs, self.sound_checkbox, self.save_button, self.fullscreen_checkbox, self.stats_window, self.talents_window, self.quests_window, self.controls_window ] + self.toggle_buttons for component in simple_components: component.render() self.screen_render.rect(COLOR_BORDER, self.ui_screen_area, 1) self.screen_render.rect_transparent(Rect(1, 1, 70, 20), 100, COLOR_BLACK) self.screen_render.text(self.font_debug_info, "fps: " + self.fps_string, (6, 4)) if self.game_mode_string: self.screen_render.rect_transparent(Rect(1, 23, 70, 20), 100, COLOR_BLACK) self.screen_render.text(self.font_debug_info, self.game_mode_string, (6, 26)) self.message.render(self.info_message.message) # TODO Bring back relative render position for dragged entities if self.item_slot_being_dragged: self._render_item_being_dragged( self.item_slot_being_dragged.item_id, self.mouse_screen_position, (UI_ICON_SIZE[0] // 2, (UI_ICON_SIZE[1] // 2))) elif self.consumable_slot_being_dragged: self._render_consumable_being_dragged( self.consumable_slot_being_dragged.consumable_types[0], self.mouse_screen_position, (UI_ICON_SIZE[0] // 2, (UI_ICON_SIZE[1] // 2))) self.dialog.render() if self.hovered_component and self.hovered_component.tooltip and not self.item_slot_being_dragged \ and not self.consumable_slot_being_dragged: tooltip: TooltipGraphics = self.hovered_component.tooltip tooltip.render() self.paused_splash_screen.render()
class GameWorldView: def __init__(self, pygame_screen, camera_size: Tuple[int, int], screen_size: Tuple[int, int], images_by_sprite: Dict[Sprite, Dict[Direction, List[ImageWithRelativePosition]]]): pygame.font.init() self.screen_render = DrawableArea(pygame_screen) self.ui_render = DrawableArea(pygame_screen, self._translate_ui_position_to_screen) self.world_render = DrawableArea(pygame_screen, self._translate_world_position_to_screen) self.ui_screen_area = Rect(0, camera_size[1], screen_size[0], screen_size[1] - camera_size[1]) self.camera_size = camera_size self.screen_size = screen_size self.font_npc_action = pygame.font.Font(DIR_FONTS + 'Monaco.dfont', 12) self.font_debug_info = pygame.font.Font(DIR_FONTS + 'Arial Rounded Bold.ttf', 19) self.font_visual_text_small = pygame.font.Font(DIR_FONTS + 'Courier New Bold.ttf', 12) self.font_visual_text = pygame.font.Font(DIR_FONTS + 'Courier New Bold.ttf', 14) self.font_visual_text_large = pygame.font.Font(DIR_FONTS + 'Courier New Bold.ttf', 16) self.font_quest_giver_mark = pygame.font.Font(DIR_FONTS + 'Courier New Bold.ttf', 28) self.images_by_sprite: Dict[Sprite, Dict[Direction, List[ImageWithRelativePosition]]] = images_by_sprite # This is updated every time the view is called self.camera_world_area = None # ------------------------------------ # TRANSLATING COORDINATES # ------------------------------------ def _translate_world_position_to_screen(self, world_position): return (self._translate_world_x_to_screen(world_position[0]), self._translate_world_y_to_screen(world_position[1])) def _translate_screen_position_to_world(self, screen_position): return int(screen_position[0] + self.camera_world_area.x), int(screen_position[1] + self.camera_world_area.y) def _translate_world_x_to_screen(self, world_x): return int(world_x - self.camera_world_area.x) def _translate_world_y_to_screen(self, world_y): return int(world_y - self.camera_world_area.y) def _translate_ui_position_to_screen(self, position): return position[0] + self.ui_screen_area.x, position[1] + self.ui_screen_area.y def _translate_screen_position_to_ui(self, position: Tuple[int, int]): return position[0] - self.ui_screen_area.x, position[1] - self.ui_screen_area.y # ------------------------------------ # DRAWING THE GAME WORLD # ------------------------------------ def _world_ground(self, entire_world_area: Rect): grid_width = 35 # TODO num squares should depend on map size. Ideally this dumb looping logic should change. num_squares = 200 column_screen_y_0 = self._translate_world_y_to_screen(self.camera_world_area.y) column_screen_y_1 = self._translate_world_y_to_screen( min(entire_world_area.y + entire_world_area.h, self.camera_world_area.y + self.camera_world_area.h)) for i_col in range(num_squares): world_x = entire_world_area.x + i_col * grid_width if entire_world_area.x < world_x < entire_world_area.x + entire_world_area.w: screen_x = self._translate_world_x_to_screen(world_x) self.screen_render.line(COLOR_BACKGROUND_LINES, (screen_x, column_screen_y_0), (screen_x, column_screen_y_1), 1) row_screen_x_0 = self._translate_world_x_to_screen(self.camera_world_area.x) row_screen_x_1 = self._translate_world_x_to_screen( min(entire_world_area.x + entire_world_area.w, self.camera_world_area.x + self.camera_world_area.w)) for i_row in range(num_squares): world_y = entire_world_area.y + i_row * grid_width if entire_world_area.y < world_y < entire_world_area.y + entire_world_area.h: screen_y = self._translate_world_y_to_screen(world_y) self.screen_render.line(COLOR_BACKGROUND_LINES, (row_screen_x_0, screen_y), (row_screen_x_1, screen_y), 1) if RENDER_WORLD_COORDINATES: for i_col in range(num_squares): for i_row in range(num_squares): if i_col % 4 == 0 and i_row % 4 == 0: world_x = entire_world_area.x + i_col * grid_width screen_x = self._translate_world_x_to_screen(world_x) world_y = entire_world_area.y + i_row * grid_width screen_y = self._translate_world_y_to_screen(world_y) self.screen_render.text(self.font_debug_info, str(world_x) + "," + str(world_y), (screen_x, screen_y), (250, 250, 250)) def _world_entity(self, entity: Union[WorldEntity, DecorationEntity]): if not entity.visible: return if entity.sprite is None: raise Exception("Entity has no sprite value: " + str(entity)) elif entity.sprite in self.images_by_sprite: image_with_relative_position = self._get_image_for_sprite( entity.sprite, entity.direction, entity.movement_animation_progress) self.world_render.image_with_relative_pos(image_with_relative_position, entity.get_position()) elif entity.sprite == Sprite.NONE: # This value is used by entities that don't use sprites. They might have other graphics (like VisualEffects) pass else: raise Exception("Unhandled sprite: " + str(entity.sprite)) def _get_image_for_sprite(self, sprite: Sprite, direction: Direction, animation_progress: float) -> ImageWithRelativePosition: images: Dict[Direction, List[ImageWithRelativePosition]] = self.images_by_sprite[sprite] if direction in images: images_for_this_direction = images[direction] else: images_for_this_direction = next(iter(images.values())) animation_frame_index = int(len(images_for_this_direction) * animation_progress) return images_for_this_direction[animation_frame_index] def _visual_effect(self, visual_effect): if isinstance(visual_effect, VisualLine): self._visual_line(visual_effect) elif isinstance(visual_effect, VisualCircle): self._visual_circle(visual_effect) elif isinstance(visual_effect, VisualRect): self._visual_rect(visual_effect) elif isinstance(visual_effect, VisualCross): self._visual_cross(visual_effect) elif isinstance(visual_effect, VisualText): self._visual_text(visual_effect) elif isinstance(visual_effect, VisualSprite): self._visual_sprite(visual_effect) elif isinstance(visual_effect, VisualParticleSystem): self._visual_particle_system(visual_effect) else: raise Exception("Unhandled visual effect: " + str(visual_effect)) def _visual_line(self, line: VisualLine): self.world_render.line(line.color, line.start_position, line.end_position, line.line_width) def _visual_circle(self, visual_circle: VisualCircle): position = visual_circle.circle()[0] radius = visual_circle.circle()[1] self.world_render.circle(visual_circle.color, position, radius, visual_circle.line_width) def _visual_rect(self, visual_rect: VisualRect): self.world_render.rect(visual_rect.color, visual_rect.rect(), visual_rect.line_width) def _visual_cross(self, visual_cross: VisualCross): for start_pos, end_pos in visual_cross.lines(): self.world_render.line(visual_cross.color, start_pos, end_pos, visual_cross.line_width) def _visual_text(self, visual_text: VisualText): text = visual_text.text position = visual_text.position() # Adjust position so that long texts don't appear too far to the right translated_position = (position[0] - 3 * len(text), position[1]) # limit the space long texts claim on the screen (example "BLOCK" and "DODGE") if len(text) >= 4: font = self.font_visual_text_small elif visual_text.emphasis: font = self.font_visual_text_large else: font = self.font_visual_text self.world_render.text(font, text, translated_position, visual_text.color) def _visual_sprite(self, visual_sprite: VisualSprite): position = visual_sprite.position animation_progress = visual_sprite.animation_progress() sprite = visual_sprite.sprite if sprite in self.images_by_sprite: image_with_relative_position = self._get_image_for_sprite( sprite, Direction.DOWN, animation_progress) self.world_render.image_with_relative_pos(image_with_relative_position, position) else: raise Exception("Unhandled sprite: " + str(sprite)) def _visual_particle_system(self, visual_particle_system: VisualParticleSystem): for particle in visual_particle_system.particles(): self.world_render.rect_transparent(particle.rect, particle.alpha, particle.color) def _stat_bar_for_world_entity(self, world_entity, h, relative_y, ratio, color, border_color=None): self.world_render.stat_bar(world_entity.x + 1, world_entity.y + relative_y, world_entity.pygame_collision_rect.w - 2, h, ratio, color, border_color=border_color) def _entity_action_text(self, entity_action_text: EntityActionText): entity_center_pos = entity_action_text.entity.get_center_position() header_prefix = "[Space] " header_line = header_prefix + entity_action_text.text detail_lines = [] for detail_entry in entity_action_text.details: detail_lines += split_text_into_lines(detail_entry, 30) if detail_lines: line_length = max(max([len(line) for line in detail_lines]), len(header_line)) else: line_length = len(header_line) rect_width = line_length * 8 rect_height = 16 + len(detail_lines) * 16 rect_pos = (entity_center_pos[0] - rect_width // 2, entity_center_pos[1] - 60) self.world_render.rect_transparent(Rect(rect_pos[0], rect_pos[1], rect_width, rect_height), 150, (0, 0, 0)) if entity_action_text.style == EntityActionTextStyle.LOOT_RARE: color = (190, 150, 250) elif entity_action_text.style == EntityActionTextStyle.LOOT_UNIQUE: color = (250, 250, 150) else: color = (255, 255, 255) self.world_render.text(self.font_npc_action, header_prefix, (rect_pos[0] + 4, rect_pos[1])) prefix_w = self.font_npc_action.size(header_prefix)[0] self.world_render.text(self.font_npc_action, entity_action_text.text, (rect_pos[0] + 4 + prefix_w, rect_pos[1]), color) for i, detail_line in enumerate(detail_lines): self.world_render.text(self.font_npc_action, detail_line, (rect_pos[0] + 4, rect_pos[1] + (i + 1) * 16)) def _quest_giver_mark(self, npc: NonPlayerCharacter): entity_pos = npc.world_entity.get_center_position() if npc.quest_giver_state == QuestGiverState.CAN_GIVE_NEW_QUEST: color = (255, 215, 0) mark = "!" elif npc.quest_giver_state == QuestGiverState.WAITING_FOR_PLAYER: color = (192, 192, 192) mark = "?" else: color = (255, 215, 0) mark = "?" self.world_render.text(self.font_quest_giver_mark, mark, (entity_pos[0] - 8, entity_pos[1] - 64), (0, 0, 0)) self.world_render.text(self.font_quest_giver_mark, mark, (entity_pos[0] - 9, entity_pos[1] - 65), color) def render_world(self, all_entities_to_render: List[WorldEntity], decorations_to_render: List[DecorationEntity], camera_world_area, non_player_characters: List[NonPlayerCharacter], is_player_invisible: bool, player_active_buffs: List[BuffWithDuration], player_entity: WorldEntity, visual_effects, render_hit_and_collision_boxes, player_health, player_max_health, entire_world_area: Rect, entity_action_text: Optional[EntityActionText]): self.camera_world_area = camera_world_area self.screen_render.fill(COLOR_BACKGROUND) self._world_ground(entire_world_area) all_entities_to_render.sort(key=lambda entry: (-entry.view_z, entry.y)) for decoration_entity in decorations_to_render: self._world_entity(decoration_entity) for entity in all_entities_to_render: self._world_entity(entity) if entity == player_entity and is_player_invisible: self.world_render.rect((200, 100, 250), player_entity.rect(), 2) player_sprite_y_relative_to_entity = \ ENTITY_SPRITE_INITIALIZERS[player_entity.sprite][Direction.DOWN].position_relative_to_entity[1] if player_entity.visible: self._stat_bar_for_world_entity(player_entity, 5, player_sprite_y_relative_to_entity - 5, player_health / player_max_health, (100, 200, 0)) # Buffs related to channeling something are rendered above player's head with progress from left to right for buff in player_active_buffs: if buff.buff_effect.get_buff_type() in CHANNELING_BUFFS: ratio = 1 - buff.get_ratio_duration_remaining() self._stat_bar_for_world_entity(player_entity, 3, player_sprite_y_relative_to_entity - 11, ratio, (150, 150, 250)) if render_hit_and_collision_boxes: for entity in all_entities_to_render: self.world_render.rect((250, 250, 250), entity.rect(), 1) for npc in non_player_characters: if npc.is_enemy: if npc.is_boss: healthbar_color = (255, 215, 0) border_color = COLOR_RED else: healthbar_color = COLOR_RED border_color = None else: healthbar_color = (250, 250, 0) border_color = None if npc.quest_giver_state is not None: self._quest_giver_mark(npc) npc_sprite_y_relative_to_entity = \ ENTITY_SPRITE_INITIALIZERS[npc.world_entity.sprite][Direction.DOWN].position_relative_to_entity[1] if not npc.is_neutral: self._stat_bar_for_world_entity(npc.world_entity, 3, npc_sprite_y_relative_to_entity - 5, npc.health_resource.get_partial(), healthbar_color, border_color) if npc.active_buffs: buff = npc.active_buffs[0] if buff.should_duration_be_visualized_on_enemies(): self._stat_bar_for_world_entity(npc.world_entity, 2, npc_sprite_y_relative_to_entity - 9, buff.get_ratio_duration_remaining(), (250, 250, 250)) for visual_effect in visual_effects: self._visual_effect(visual_effect) if entity_action_text: self._entity_action_text(entity_action_text)
class MapEditorView: def __init__(self, pygame_screen, camera_size: Tuple[int, int], screen_size: Tuple[int, int], images_by_sprite: Dict[Sprite, Dict[Direction, List[ImageWithRelativePosition]]], images_by_ui_sprite: Dict[UiIconSprite, Any], images_by_portrait_sprite: Dict[PortraitIconSprite, Any]): self.camera_size = camera_size self.screen_size = screen_size self.screen_render = DrawableArea(pygame_screen) self.images_by_sprite = images_by_sprite self.images_by_ui_sprite = images_by_ui_sprite self.images_by_portrait_sprite = images_by_portrait_sprite self.ui_screen_area = Rect(0, camera_size[1], screen_size[0], screen_size[1] - camera_size[1]) self.screen_render = DrawableArea(pygame_screen) self.ui_render = DrawableArea(pygame_screen, self._translate_ui_position_to_screen) self.font_debug_info = pygame.font.Font( DIR_FONTS + 'Courier New Bold.ttf', 12) self.font_ui_icon_keys = pygame.font.Font( DIR_FONTS + 'Courier New Bold.ttf', 12) w_tab_button = 75 self.tab_buttons = { EntityTab.ITEMS: RadioButton(self.ui_render, Rect(300, 10, w_tab_button, 20), "ITEMS (V)"), EntityTab.NPCS: RadioButton(self.ui_render, Rect(380, 10, w_tab_button, 20), "NPCS (B)"), EntityTab.WALLS: RadioButton(self.ui_render, Rect(460, 10, w_tab_button, 20), "WALLS (N)"), EntityTab.MISC: RadioButton(self.ui_render, Rect(540, 10, w_tab_button, 20), "MISC. (M)"), } self.shown_tab: EntityTab = EntityTab.ITEMS self.checkbox_show_entity_outlines = Checkbox(self.ui_render, Rect(15, 100, 120, 20), "outlines", True) self.checkboxes = [self.checkbox_show_entity_outlines] self.mouse_screen_position = (0, 0) self.hovered_component = None def handle_mouse_movement(self, mouse_screen_pos: Tuple[int, int]): self.mouse_screen_position = mouse_screen_pos mouse_ui_position = self._translate_screen_position_to_ui( mouse_screen_pos) for checkbox in self.checkboxes: if checkbox.contains(mouse_ui_position): self._on_hover_component(checkbox) def _on_hover_component(self, component): self._set_currently_hovered_component_not_hovered() self.hovered_component = component self.hovered_component.hovered = True def _set_currently_hovered_component_not_hovered(self): if self.hovered_component is not None: self.hovered_component.hovered = False self.hovered_component = None def handle_mouse_click(self): if self.hovered_component in self.checkboxes: self.hovered_component.on_click() def _translate_ui_position_to_screen(self, position): return position[0] + self.ui_screen_area.x, position[ 1] + self.ui_screen_area.y def _translate_screen_position_to_ui(self, position: Tuple[int, int]): return position[0] - self.ui_screen_area.x, position[ 1] - self.ui_screen_area.y def _get_image_for_sprite( self, sprite: Sprite, direction: Direction, animation_progress: float) -> ImageWithRelativePosition: images: Dict[ Direction, List[ImageWithRelativePosition]] = self.images_by_sprite[sprite] if direction in images: images_for_this_direction = images[direction] else: images_for_this_direction = next(iter(images.values())) animation_frame_index = int( len(images_for_this_direction) * animation_progress) return images_for_this_direction[animation_frame_index] def set_shown_tab(self, shown_tab: EntityTab): self.tab_buttons[self.shown_tab].enabled = False self.shown_tab = shown_tab self.tab_buttons[shown_tab].enabled = True def render( self, entities: List[MapEditorWorldEntity], placing_entity: Optional[MapEditorWorldEntity], deleting_entities: bool, deleting_decorations: bool, num_enemies: int, num_walls: int, num_decorations: int, grid_cell_size: int, mouse_screen_position: Tuple[int, int], camera_rect_ratio: Tuple[float, float, float, float], npc_positions_ratio: List[Tuple[float, float]], wall_positions_ratio: List[Tuple[float, float]] ) -> Optional[MapEditorWorldEntity]: mouse_ui_position = self._translate_screen_position_to_ui( mouse_screen_position) hovered_by_mouse: MapEditorWorldEntity = None self.screen_render.rect( COLOR_BLACK, Rect(0, 0, self.camera_size[0], self.camera_size[1]), 3) self.screen_render.rect_filled( COLOR_BLACK, Rect(0, self.camera_size[1], self.screen_size[0], self.screen_size[1] - self.camera_size[1])) icon_space = 5 y_1 = 10 y_2 = y_1 + 30 x_0 = 20 # TODO Handle all these icons as state, similarly to how the game UI is done, and the tab radio buttons self._map_editor_icon_in_ui(x_0, y_2, MAP_EDITOR_UI_ICON_SIZE, deleting_entities, 'Q', None, UiIconSprite.MAP_EDITOR_TRASHCAN) self._map_editor_icon_in_ui( x_0 + MAP_EDITOR_UI_ICON_SIZE[0] + icon_space, y_2, MAP_EDITOR_UI_ICON_SIZE, deleting_decorations, 'Z', None, UiIconSprite.MAP_EDITOR_RECYCLING) x_1 = 155 num_icons_per_row = 23 for i, entity in enumerate(entities): is_this_entity_being_placed = entity is placing_entity x = x_1 + (i % num_icons_per_row) * (MAP_EDITOR_UI_ICON_SIZE[0] + icon_space) row_index = (i // num_icons_per_row) y = y_2 + row_index * (MAP_EDITOR_UI_ICON_SIZE[1] + icon_space) if is_point_in_rect( mouse_ui_position, Rect(x, y, MAP_EDITOR_UI_ICON_SIZE[0], MAP_EDITOR_UI_ICON_SIZE[1])): hovered_by_mouse = entity self._map_editor_icon_in_ui(x, y, MAP_EDITOR_UI_ICON_SIZE, is_this_entity_being_placed, '', entity.sprite, None) self.screen_render.rect(COLOR_WHITE, self.ui_screen_area, 1) self.screen_render.rect_transparent(Rect(0, 0, 150, 80), 100, COLOR_BLACK) self.screen_render.text(self.font_debug_info, "# enemies: " + str(num_enemies), (5, 3)) self.screen_render.text(self.font_debug_info, "# walls: " + str(num_walls), (5, 20)) self.screen_render.text(self.font_debug_info, "# decorations: " + str(num_decorations), (5, 37)) self.screen_render.text(self.font_debug_info, "Cell size: " + str(grid_cell_size), (5, 54)) self._render_minimap(camera_rect_ratio, npc_positions_ratio, wall_positions_ratio) for button in self.tab_buttons.values(): button.render() for checkbox in self.checkboxes: checkbox.render() return hovered_by_mouse def _render_minimap(self, camera_rect_ratio, npc_positions_ratio, wall_positions_ratio): rect = Rect(self.screen_size[0] - 180, self.screen_size[1] - 180, 160, 160) border_width = 1 self.screen_render.rect( COLOR_WHITE, Rect(rect.x - border_width, rect.y - border_width, rect.w + border_width * 2, rect.h + border_width * 2), border_width) for npc_pos in npc_positions_ratio: self.screen_render.rect((200, 200, 200), Rect(rect.x + npc_pos[0] * rect.w, rect.y + npc_pos[1] * rect.h, 1, 1), 1) for wall_pos in wall_positions_ratio: self.screen_render.rect((100, 100, 150), Rect(rect.x + wall_pos[0] * rect.w, rect.y + wall_pos[1] * rect.h, 1, 1), 1) self.screen_render.rect((150, 250, 150), Rect(rect.x + camera_rect_ratio[0] * rect.w, rect.y + camera_rect_ratio[1] * rect.h, camera_rect_ratio[2] * rect.w, camera_rect_ratio[3] * rect.h), 1) def render_map_editor_mouse_rect(self, color: Tuple[int, int, int], map_editor_mouse_rect: Rect): self.screen_render.rect(color, map_editor_mouse_rect, 3) def render_map_editor_world_entity_at_position(self, sprite: Sprite, entity_size: Tuple[int, int], position: Tuple[int, int]): image_with_relative_position = self._get_image_for_sprite( sprite, Direction.DOWN, 0) sprite_position = sum_of_vectors( position, image_with_relative_position.position_relative_to_entity) self.screen_render.image(image_with_relative_position.image, sprite_position) self.screen_render.rect((50, 250, 0), Rect(position[0], position[1], entity_size[0], entity_size[1]), 3) def _map_editor_icon_in_ui(self, x, y, size: Tuple[int, int], highlighted: bool, user_input_key: str, sprite: Optional[Sprite], ui_icon_sprite: Optional[UiIconSprite]): w = size[0] h = size[1] self.ui_render.rect_filled((40, 40, 40), Rect(x, y, w, h)) if sprite: image = self.images_by_sprite[sprite][Direction.DOWN][0].image elif ui_icon_sprite: image = self.images_by_ui_sprite[ui_icon_sprite] else: raise Exception("Nothing to render!") icon_scaled_image = pygame.transform.scale(image, size) self.ui_render.image(icon_scaled_image, (x, y)) self.ui_render.rect(COLOR_WHITE, Rect(x, y, w, h), 1) if highlighted: self.ui_render.rect(COLOR_HIGHLIGHTED_ICON, Rect(x - 1, y - 1, w + 2, h + 2), 3) self.ui_render.text(self.font_ui_icon_keys, user_input_key, (x + 12, y + h + 4)) def is_screen_position_within_ui(self, screen_position: Tuple[int, int]): ui_position = self._translate_screen_position_to_ui(screen_position) return ui_position[1] >= 0