def draw_held_entity(self): if self.held_entity is None: return s = self.camera.get_cell_size_px() rect = pg.Rect(*self.mouse_pos, s + 1, s + 1) rect.move_ip(*(-self.hold_point * s)) self.held_entity.draw_onto(self.screen, rect, self.edit_mode) # pass in True here to show selection highlight # TEMPORARY: copied from `render_board` s = self.camera.get_cell_size_px() def grid_to_px(pos: V2) -> V2: return (V2(*self.viewport_surf.get_rect().center) + (pos - self.camera.center) * s).floor() # draw wiring while moving entity if self.held_entity.has_ports: e = self.held_entity wire_width = self.camera.get_wire_width() for index, (is_input, f, f_index) in enumerate(e.wirings): if f is None: continue f_pos = self.level.board.find(f) if f_pos is None: continue # raise RuntimeError("unable to find desired entity while drawing wiring") start_offset = e.get_port_offset(is_input, index) end_offset = f.get_port_offset(not is_input, f_index) start = V2(*rect.topleft) + V2(rect.width * start_offset[0], rect.height * start_offset[1]) end = grid_to_px(f_pos + end_offset) color = WIRE_COLOR_ON if e.port_states[index] else WIRE_COLOR_OFF pg.draw.line(self.screen, color, tuple(start), tuple(end), wire_width)
def get_port_offset(self, is_input, index): if is_input: gate_h = 1.0 - 2 * self.top_bottom_padding return V2( self.left_right_padding, self.top_bottom_padding + gate_h * (index + 1) / (self.num_inputs + 1)) else: return V2(1.0 - self.left_right_padding, 0.5)
def get_all(self, filter_type: Type[Entity] = None): """returns a generator containing all present entities (with positions)""" if filter_type is None: for pos, cell in self.cells.items(): for e in cell: yield V2(*pos), e else: for e_type in self.type_locs: if issubclass(e_type, filter_type): for pos, e in self.type_locs[e_type]: yield V2(*pos), e
def draw_onto(self, surf: pg.Surface, rect: pg.Rect, **kwargs): super().draw_onto(surf, rect) s = rect.width compass_center = round(V2(rect.centerx, rect.centery - s * 0.1)) # pg.draw.circle(surf, (0, 0, 0), tuple(compass_center), round(s * 0.1), width=round(s * 0.05)) # draw_aacircle(surf, *compass_center, round(s * 0.05), (0, 0, 0)) self.hitboxes = [(d, draw_chevron(surf, compass_center + d * s * 0.25, d, (255, 255, 255) if self.get_value() is d else (0, 0, 0), s * 0.12, s * 0.04)) for d in Direction if d is not Direction.NONE] render_text_centered_xy(self.label, (0, 0, 0), surf, V2(rect.centerx, rect.bottom - s * 0.16), FONT_SIZE)
def handle_events(self, events): for event in events: if event.type == pg.QUIT: self.running = False elif event.type == pg.VIDEORESIZE: self.handle_window_resize(event.w, event.h) elif event.type == pg.KEYDOWN: # --------- TEMPORARY -------# if event.key in (pg.K_RETURN, pg.K_KP_ENTER): self.level.won = True print("FORCING LEVEL WIN") # ---------------------------# self.keys_pressed.add(event.key) self.handle_keydown(event.key) elif event.type == pg.KEYUP: self.keys_pressed.discard(event.key) self.handle_keyup(event.key) elif event.type == pg.MOUSEBUTTONDOWN: self.mouse_buttons_pressed.add(event.button) self.handle_mousebuttondown(event.button) elif event.type == pg.MOUSEBUTTONUP: self.mouse_buttons_pressed.discard(event.button) self.handle_mousebuttonup(event.button) elif event.type == pg.MOUSEMOTION: self.mouse_pos = V2(*event.pos) self.handle_mousemotion(event.rel) self.handle_keys_pressed() return set() # we consume all events
def take_snapshot(self, entity, dims) -> pg.Surface: surf = pg.Surface(dims) if entity is self.level_runner.held_entity: return self.take_snapshot_at_mouse(dims) # # TODO: maybe draw a hand icon here showing that the entity is being held (?) # # for now, draw a fake empty board # cam = Camera(V2(0.5, 0.5), self.zoom_level) # render_board( # Board(), surf, cam, # selected_entity=self.level_runner.selected_entity, # substep_progress=self.level_runner.substep_progress # ) else: pos = self.level_runner.level.board.find(entity) if pos is None: raise ValueError( f"desired entity not found while taking snapshot ({entity})" ) cam = Camera(pos + V2(0.5, 0.5), self.zoom_level) render_board(self.level_runner.level.board, surf, cam, selected_entity=self.level_runner.selected_entity, substep_progress=self.level_runner.substep_progress) return surf
def reset_level(self): self.shelf_height_onscreen = SHELF_HEIGHT self.edit_mode = True self.paused = False self.fast_forward = False self.slow_motion = False self.wiring_widget: Optional[WireEditor] = None # if not None, the WireEditor that is currently being used self.shelf_state = "open" # "open", "closed", "opening", or "closing" self.editor_state = "closed" # "open", "closed", "opening", or "closing" self.editor_state_queue = [] self.substep_progress = 0.0 # float in [0, 1) denoting fraction of current step completed (for animation) self.editor_width_onscreen = 0 self.editor_content_height = None self.editor_scroll_amt = 0 self.read_only_indicator_blink_frames = 0 # initialize camera to contain `level.board` (with some margin) rect = self.level.board.get_bounding_rect(margin=3) # arbitrary value zoom_level = min(self.screen_width / rect.width, self.screen_height / rect.height) / DEFAULT_CELL_SIZE self.camera = Camera(center=V2(*rect.center), zoom_level=zoom_level) # initialize refresh sentinels self.window_size_changed = True self.viewport_changed = True self.shelf_changed = True self.editor_changed = True self.reblit_needed = True self.held_entity: Union[Entity, None] = None self.hold_point: V2 = V2(0, 0) # in [0, 1]^2 self.selected_entity: Union[Entity, None] = None self.editing_entity: Union[Entity, None] = None # self.selected_entity_queue = [] self.pressed_icon: Optional[str] = None self.current_modal: Union[Modal, None] = None
def __init__(self, color: Color, velocity: Direction = Direction.NONE, locked: bool = False, **kwargs): super().__init__(locked, **kwargs) self.color = color self.velocity = velocity self.leaky = False self.draw_center = V2(0, 0)
def draw_onto_base(self, surf: pg.Surface, rect: pg.Rect, edit_mode: bool, step_progress: float = 0.0, neighborhood=(([], ) * 5, ) * 5): s = rect.width self.draw_center = V2(*rect.center) for anim in self.animations: if anim[0] == "translate": amt = self.travel_curve(step_progress) self.draw_center += anim[1] * (s - 1) * (amt - 1) elif anim[0] == "shift": a = 0.18 if step_progress < a: amt = 0 elif step_progress < 0.5: amt = (step_progress - a) * 2 * Piston.max_amt else: amt = self.travel_curve(step_progress) self.draw_center += anim[1] * (s - 1) * (amt - 1) draw_radius = s * 0.3 draw_color_rgb = self.color.rgb() # check for intersection with other barrel # if intersection found, take weighted average of colors n = len(neighborhood) for x in range(-n // 2, n // 2 + 1): for y in range(-n // 2, n // 2 + 1): for e in neighborhood[y + n // 2][x + n // 2]: if e is self: continue # skip self if isinstance(e, Barrel): dist = (e.draw_center - self.draw_center).length() if dist < draw_radius * 2: percentage = 1.0 - dist / (2 * draw_radius) # print(f"intersecting by {dist} pixels ({percentage * 100:.0f}%)") # smoothly transition towards merged color draw_color_rgb = interpolate_colors( self.color.rgb(), (self.color + e.color).rgb(), percentage) # pg.draw.circle(surf, draw_color_rgb, tuple(self.draw_center), draw_radius) draw_aacircle(surf, *round(self.draw_center), round(draw_radius), draw_color_rgb) if edit_mode: draw_chevron(surf, self.draw_center + self.velocity * (s * 0.42), self.velocity, VELOCITY_CHEVRON_COLOR, round(s * 0.25), round(s**0.5 * 0.4), angle=120)
def draw_onto_base(self, surf: pg.Surface, rect: pg.Rect, edit_mode: bool, step_progress: float = 0.0, neighborhood=(([], ) * 5, ) * 5): s = rect.width for i in range(3): draw_chevron( surf, V2(*rect.center) + self.orientation * (i - 0.4) * (s // 5), self.orientation, (0, 0, 0), s // 3, round(s * 0.05))
def handle_mousemotion(self, rel): # pan camera if right click is held if 3 in self.mouse_buttons_pressed: disp = V2(*rel) / self.camera.get_cell_size_px() self.camera.pan_abs(-disp) self.viewport_changed = True if self.held_entity is not None: self.reblit_needed = True self.editor_changed = True if self.wiring_widget is not None: self.editor_changed = True
def get_cells(self, window: pg.Rect = None): """returns a generator containing all non-empty cells (along with positions) contained within `window`""" if window is None: for pos, cell in self.cells.items(): yield V2(*pos), cell else: # choose the most efficient iteration method # if this ever proves insufficient (doubtful), use a more efficient data structure (e.g. 2D range tree) if window.width * window.height < self.get_cell_count(): for x in range(window.width): for y in range(window.height): pos = (window.left + x, window.top + y) if pos in self.cells: yield V2(*pos), self.cells[pos] return ( V2(*window.topleft) + V2(x, y) for y in range(window.height) for x in range(window.width) ) else: # when window is large, this will almost always be preferred for pos, cell in self.cells.items(): if window.collidepoint(*pos): yield V2(*pos), cell
def draw_onto_base(self, surf: pg.Surface, rect: pg.Rect, edit_mode: bool, step_progress: float = 0.0, neighborhood=(([], ) * 5, ) * 5): # TEMPORARY s = rect.width w = round(s * 0.1) draw_aacircle(surf, *rect.center, round(s * 0.35), (210, 210, 210)) draw_chevron(surf, V2(*rect.center) + self.orientation * (s * 0.432), self.orientation, (210, 210, 210), round(s * 0.28), w, angle=108)
def __init__(self, level_queue: List[Level], postprocessing_effects: Sequence[PostprocessingEffect]=[]): self.level_queue = level_queue self.postprocessing_effects = postprocessing_effects self.screen_width = DEFAULT_SCREEN_WIDTH self.screen_height = DEFAULT_SCREEN_HEIGHT self.advance_level() self.keys_pressed = set() self.mouse_buttons_pressed = set() self.mouse_pos = V2(0, 0) self.palette_rects: Sequence[Tuple[pg.Rect, Type[Entity]]] = [] # store palette item rects for easier collision self.widget_rects: Sequence[Tuple[pg.Rect, Widget]] = [] # store widget rects for easier collision self.shelf_icon_rects: Sequence[Tuple[pg.Rect, str]] = [] # store shelf icon rects for easier collision self.snapshot_provider = SnapshotProvider(self)
def draw_onto_base(self, surf: pg.Surface, rect: pg.Rect, edit_mode: bool, step_progress: float = 0.0, neighborhood=(([], ) * 5, ) * 5): eye_box_width = rect.width * 0.75 pupil_radius = rect.width * 0.15 angular_width = pi * 0.63 draw_width = round(rect.width * 0.04) num_lines = 5 line_length = rect.width * 0.15 # draw edges of eye pg.draw.arc(surf, (0, 0, 0), pg.Rect(rect.left + (rect.width - eye_box_width) / 2, rect.centery - pupil_radius, eye_box_width, rect.height / 2 + pupil_radius), pi / 2 - angular_width / 2, pi / 2 + angular_width / 2, width=draw_width) pg.draw.arc(surf, (0, 0, 0), pg.Rect(rect.left + (rect.width - eye_box_width) / 2, rect.top, eye_box_width, rect.height / 2 + pupil_radius), -pi / 2 - angular_width / 2, -pi / 2 + angular_width / 2, width=draw_width) # draw pupil pg.draw.circle(surf, (0, 0, 0), rect.center, pupil_radius) for i in range(num_lines): theta = 90 / num_lines * (i - num_lines // 2) offset = self.orientation.rotate(round(theta)) start = V2(*rect.center) + offset * pupil_radius * 1.4 end = start + offset * line_length pg.draw.line(surf, (0, 0, 0), tuple(start), tuple(end), width=round(draw_width / 2))
def draw_onto(self, surf: pg.Surface, rect: pg.Rect, snapshot_provider=None) -> None: super().draw_onto(surf, rect) render_text_left_justified( self.label, (0, 0, 0), surf, V2(rect.left + rect.width * 0.03, rect.centery), FONT_SIZE) w = rect.width * 0.45 h = w # rect.height * 0.9 self.snapshot_rect = pg.Rect(rect.left + rect.width * 0.70 - w / 2, rect.top + (rect.height - h) / 2, w, h) # draw background pg.draw.rect(surf, (255, 255, 255), self.snapshot_rect) if self.get_value()[0] is None and not self.in_use: size = FONT_SIZE * 0.65 render_text_centered_xy("not", (63, 63, 63), surf, (self.snapshot_rect.centerx, self.snapshot_rect.centery - size / 2), size) render_text_centered_xy("connected", (63, 63, 63), surf, (self.snapshot_rect.centerx, self.snapshot_rect.centery + size / 2), size) else: if self.in_use: snap = snapshot_provider.take_snapshot_at_mouse( self.snapshot_rect.size) else: snap = snapshot_provider.take_snapshot(self.get_value()[0], self.snapshot_rect.size) surf.blit(snap, self.snapshot_rect) # draw border color = HIGHLIGHT_COLOR if self.in_use else (0, 0, 0) pg.draw.rect(surf, color, self.snapshot_rect, 2)
def draw_onto(self, surf: pg.Surface, rect: pg.Rect, **kwargs): super().draw_onto(surf, rect) self.set_value(clamp(self.get_value(), *self.get_limits())) render_text_left_justified( self.label, (0, 0, 0), surf, V2(rect.left + rect.width * 0.03, rect.centery), FONT_SIZE) # TODO: draw number selector boxes box_width = int(rect.height * 0.75) box_height = int(rect.height * 0.75) box_thickness = 2 self.hitboxes.clear() low, high = self.get_limits() for i, n in enumerate(range(low, high + 1)): # inclusive box = pg.Rect( rect.left + rect.width * 0.375 + (box_width - box_thickness // 2) * i, rect.centery - box_height / 2, box_width, box_height) color = (255, 255, 255) if n == self.get_value() else (0, 0, 0) draw_rectangle(surf, box, (0, 0, 0), thickness=box_thickness) render_text_centered_xy(str(n), color, surf, box.center, FONT_SIZE) self.hitboxes.append((n, box))
def render_board(board: Board, surf: pg.Surface, cam: Camera, edit_mode: bool = False, selected_entity: Entity = None, substep_progress: float = 0.0, wiring_visible: bool = True): """render `board` to `surf` with the given parameters""" # TODO: draw carpets, then grid, then blocks # z_pos: < 0 = 0 > 0 surf.fill(VIEWPORT_BG_COLOR) s = cam.get_cell_size_px() surf_center = V2(*surf.get_rect().center) surf_width, surf_height = surf.get_size() def grid_to_px(pos: V2) -> V2: return (surf_center + (pos - cam.center) * s).floor() w = surf_width / s + 2 h = surf_height / s + 2 grid_rect = pg.Rect(floor(cam.center.x - w / 2), floor(cam.center.y - h / 2), ceil(w) + 1, ceil(h) + 1) grid_line_width = cam.get_grid_line_width() # draw board for grid_pos, cell in board.get_cells(grid_rect): if not cell: continue draw_pos = grid_to_px(grid_pos) neighborhood = [[ board.get(*(grid_pos + V2(x_offset, y_offset))) for x_offset in range(-2, 3) ] for y_offset in range(-2, 3)] for e in sorted(cell, key=lambda e: e.draw_precedence): rect = pg.Rect(*draw_pos, s + 1, s + 1) e.draw_onto(surf, rect, edit_mode, selected_entity is e, substep_progress, neighborhood) # draw grid with dynamic line width for x in range(grid_rect.width): x_grid = grid_rect.left + x x_px, _ = grid_to_px(V2(x_grid, 0)) pg.draw.line(surf, GRID_LINE_COLOR, (x_px, 0), (x_px, surf_height), width=grid_line_width) for y in range(grid_rect.height): y_grid = grid_rect.top + y _, y_px = grid_to_px(V2(0, y_grid)) pg.draw.line(surf, GRID_LINE_COLOR, (0, y_px), (surf_width, y_px), width=grid_line_width) # draw wires # TODO: make lines correct thickness when slanted # TODO: antialias lines if wiring_visible: wire_width = cam.get_wire_width() for pos, e in board.get_all(filter_type=Wirable): for index, (is_input, f, f_index) in enumerate(e.wirings): if f is None: continue f_pos = board.find(f) if f_pos is None: continue # raise RuntimeError("unable to find desired entity while drawing wiring") start = grid_to_px(pos + e.get_port_offset(is_input, index)) end = grid_to_px(f_pos + f.get_port_offset(not is_input, f_index)) color = WIRE_COLOR_ON if e.port_states[ index] else WIRE_COLOR_OFF pg.draw.line(surf, color, tuple(start), tuple(end), wire_width)
def get_world_coords(self, pos: V2, screen_width, screen_height): """converts the given pixel `pos` to world coordinates""" screen_center = V2(screen_width / 2, screen_height / 2) diff = pos - screen_center return self.center + diff * (1 / self.get_cell_size_px())
def grid_to_px(pos: V2) -> V2: return (V2(*self.viewport_surf.get_rect().center) + (pos - self.camera.center) * s).floor()
def get_port_offset(self, is_input, index): return V2(0.5, 0.5)
def handle_mousebuttondown(self, button): mouse_over_shelf = self.mouse_pos.y >= self.screen_height - self.shelf_height_onscreen mouse_over_editor = self.mouse_pos.x >= self.screen_width - self.editor_width_onscreen \ and self.mouse_pos.y < self.screen_height - SHELF_HEIGHT # scroll wheel if button in (4, 5): if button == 4: scroll_direction = 1 elif button == 5: scroll_direction = -1 # handle editor scrolling editor_scrolled = False if mouse_over_editor: old_scroll_amt = self.editor_scroll_amt max_scroll_amt = self.editor_content_height - self.editor_surf.get_height() if max_scroll_amt < 0: max_scroll_amt = 0 self.editor_scroll_amt = clamp( self.editor_scroll_amt + scroll_direction * EDITOR_SCROLL_SPEED, -max_scroll_amt, 0 ) editor_scrolled = old_scroll_amt != self.editor_scroll_amt if editor_scrolled: self.editor_changed = True # # if unable to scroll editor, then zoom the camera # if not editor_scrolled: # if not over editor editor, then zoom the camera if not mouse_over_editor: zoom_direction = 0 if button == 4: # zoom in zoom_direction = 1 elif button == 5: # zoom out zoom_direction = -1 if zoom_direction != 0: pivot = self.camera.get_world_coords(self.mouse_pos, self.screen_width, self.screen_height) self.camera.zoom(zoom_direction, pivot) self.viewport_changed = True # left click if button == 1: # handle shelf icons for rect, icon in self.shelf_icon_rects: if rect.collidepoint(*self.mouse_pos): self.pressed_icon = icon self.reblit_needed = True return # if not self.edit_mode: # return entity_clicked = None clicked_wire_widget = False # handle entity holding (left click) if mouse_over_shelf: adjusted_pos = self.mouse_pos - V2(0, self.screen_height - self.shelf_height_onscreen) for rect, e_prototype in self.palette_rects: if rect.collidepoint(*adjusted_pos): # pick up entity (from palette) new = e_prototype.get_instance() # create new entity self.held_entity = new self.hold_point = V2(0.5, 0.5) self.level.palette.remove(e_prototype) self.deselect_entity() self.select_entity(new) self.shelf_changed = True break elif mouse_over_editor: if self.edit_mode: # cannot interact with editor if not in edit mode adjusted_pos = self.mouse_pos - V2(self.screen_width - self.editor_width_onscreen, 0) for hitbox, widget in self.widget_rects: if hitbox.collidepoint(*adjusted_pos): clicked_wire_widget = widget.handle_click(adjusted_pos) if clicked_wire_widget: self.finish_wiring(None) # deselect previously selected wire widget self.wiring_widget = clicked_wire_widget self.editor_changed = True # just redraw every time (easier) self.viewport_changed = True # ^^^ break else: # if not in edit mode, trigger read-only indicator to blink if self.read_only_indicator_blink_frames == 0: # if not currently blinking self.read_only_indicator_blink_frames = 2 * BLINK_DURATION # blink twice else: # mouse is over board pos_float = self.camera.get_world_coords(self.mouse_pos, self.screen_width, self.screen_height) pos = pos_float.floor() cell = self.level.board.get(*pos) if cell: entity_clicked = cell[-1] # click last element; TODO: figure out if this is a problem lol if self.wiring_widget is None: # don't change selection if in wiring mode # deselect current selection if clicking on something else if self.selected_entity is not entity_clicked: self.deselect_entity() if entity_clicked is not None and not entity_clicked.locked: # pick up entity (from board) if self.edit_mode: self.held_entity = entity_clicked self.hold_point = pos_float.fmod(1) self.level.board.remove(*pos, self.held_entity) self.viewport_changed = True if entity_clicked.editable: self.select_entity(entity_clicked) else: self.deselect_entity() if not clicked_wire_widget: self.finish_wiring(entity_clicked)