def delete_selected_text(self): if (self.selection_start != self.selection_end and self.text): console_msg("Deleting selection", 8, line_end='') # make sure the selection start is the top left of the block start_pos = (self.selection_start[Y] * self.row_width + self.selection_start[X]) end_pos = (self.selection_end[Y] * self.row_width + self.selection_end[X]) if start_pos > end_pos: temp = self.selection_start self.selection_start = self.selection_end self.selection_end = temp self.save_history() # prevent backspace() from also trying to delete the block self.deleting_block = True # make sure the cursor is positioned at the end of the selection self.cursor_col, self.cursor_line = self.selection_end # backspace all the way to the first char in the selection while (self.cursor_col, self.cursor_line) != self.selection_start: # turn off undo for the individual deletions, # since we have already saved self.backspace(undo=False) print(".", end='') # cancel this selection self.selection_start = (0, 0) self.selection_end = (0, 0) self.deleting_block = False print(" done.")
def rewind_level(self): console_msg("Rewinding!", 8) self.rewinding = True self.blocks.reset() self.player.set_position(self.blocks.get_player_start(self.puzzle)) self.dog.set_position(self.blocks.get_dog_start(self.puzzle)) self.dog.clear_speech_bubble()
def check(self, character): """ check if the trigger has activated for now this just handles the pressure plate type of trigger others will be added - possibly by subclassing this """ if self.type == 'pressure plate': trigger_rect = pygame.Rect(self.block.x, self.block.y, BLOCK_SIZE, BLOCK_SIZE) if character.location.colliderect(trigger_rect): # switch to 'pressed' state self.block.image = self.block.frames[1] if self.random: if not self.random_action[0].activated: self.random_action[0].activate(self.random_action[1]) else: # fire all actions associated with this trigger for action in self.actions: if not action[0].activated: # the action is a tuple of mover and movement # so we call the activate method of the mover # and pass the movement as the argument action[0].activate(action[1]) console_msg( "trigger " + "(" + str(self.block.x) + str(self.block.y) + ")" + " activated!", 8)
def input(self, msg=''): # get input from the user in a separate editor window self.world.input.activate('input:' + msg) while self.world.input.is_active(): self.world.update(self) result = self.world.input.convert_to_lines()[0] console_msg("input:" + str(result), 8) return result
def __init__(self, screen, height, code_font): self.code_font = code_font self.screen = screen self.width = screen.get_size()[X] self.height = height self.surface = pygame.Surface((self.width, self.height)) self.side_gutter = 8 # pixel gap from the edge of the surface self.color_modes = {0: (LIGHT_GREY, SKY_BLUE), 1: (BLACK, YELLOW), 2: (BLACK, GREEN)} self.palette = 0 self.line_height = self.code_font.get_linesize() self.top_margin = self.line_height + 4 # maximum number of lines that will fit in the editor window self.max_lines = int(self.height / self.line_height) # width of a single character # (monospaced font, so they're all the same) self.char_width = self.code_font.size(" ")[X] # the margin allows space for the line numbers in the code editor self.left_margin = self.side_gutter # calculate the number of characters that fit on a line self.row_width = int((self.width - self.left_margin - self.side_gutter) / self.char_width) self.buttons = button_tray.ButtonTray(EDITOR_ICON_FILE, self.surface) self.title = "Title" self.centre_title = False # set to true for the menu input dialog # the text is represented as a list of logical lines # each line is a list of characters # there are no line terminator characters or padding characters # we initialise with a single row self.text = [[]] # undo history is a list where each element is a copy of self.text self.history = [] # absolute line number of the cursor self.cursor_line = 0 # character position of the cursor within the current line self.cursor_col = 0 self.selecting = False # True when currently marking a block of text # cursor coords of the start and end of the marked block self.selection_start = (0, 0) self.selection_end = (0, 0) self.deleting_block = False self.v_scroll = 0 # line offset to allow text to be scrolled self.active = False self.run_enabled = False self.key_action = {} self.ctrl_shortcuts = {pygame.K_x: self.clipboard_cut, pygame.K_c: self.clipboard_copy, pygame.K_v: self.clipboard_paste, pygame.K_a: self.select_all, pygame.K_z: self.undo, } console_msg("Editor row width =" + str(self.row_width), 8)
def __init__(self, world, location, name='sentry'): super().__init__(world, name, ROBOT_SPRITE_FILE, (16, 28), 2) self.set_position(location) # DEBUG convert source to a list of chars, # so that it can be handled the same as the editor code source = 'print("hi")' self.source_code = [] line = [] for char in source: line.append(char) self.source_code.append(line) console_msg("robot source loaded", 0)
def update(self): # process all the keystrokes in the event queue printable = "1234567890-=[]#;',./abcdefghijklmnopqrstuvwxyz " \ '!"£$%^&*()_+{}~:@<>?ABCDEFGHIJKLMNOPQRSTUVWXYZ' for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key in self.key_action: # handle all the special keys self.key_action[event.key]() elif pygame.key.get_mods() & pygame.KMOD_CTRL: # handle the keyboard shortcuts if event.key in self.ctrl_shortcuts: self.ctrl_shortcuts[event.key]() else: # handle all the printable characters if event.unicode != '' and event.unicode in printable: self.add_keystroke(event.unicode) elif event.type == pygame.KEYUP: if event.key in (pygame.K_LSHIFT, pygame.K_RSHIFT): self.stop_selecting() elif event.type == pygame.MOUSEBUTTONDOWN: # check if the click happened within the editor area if pygame.mouse.get_pos()[Y] > self.height: mouse_button = {1: self.left_click, # 2: middle button # 3: right button 4: self.cursor_up, # scroll up 5: self.cursor_down, # scroll down } if event.button in mouse_button: mouse_button[event.button]() elif event.type == pygame.MOUSEBUTTONUP: self.mouse_up() # we also have to handle the quit event, since the main loop # isn't watching events while the editor is open # we don't actually want to quit the game though # TODO prompt user to save code elif event.type == pygame.QUIT: console_msg("WARNING: Save code before quitting?", 2) if self.selecting: # use the mouse to update the selected block, # provided SHIFT is not held down # this keeps mouse and keyboard selections # from interfering with each other if not (pygame.key.get_mods() & pygame.KMOD_SHIFT): self.cursor_to_mouse_pos()
def run_program(self, source): """ pass the text in the puzzle solution to the interpreter""" p = interpreter.VirtualMachine(self.robot) p.load(source) result, errors = p.compile() if result is False: # check for syntax errors # TODO display these using in-game dialogs if p.compile_time_error: error_msg = p.compile_time_error['error'] error_line = p.compile_time_error['line'] console_msg('BIT found a SYNTAX ERROR:', 5) msg = error_msg + " on line " + str(error_line) console_msg(msg, 5) else: result, errors = p.run() # set the program going
def __init__(self, world, name, sprite_file, size_in_pixels, height_in_blocks=1): super().__init__(world, name, sprite_file, size_in_pixels) self.height_in_blocks = height_in_blocks self.collidable = True self.set_trigger_test(world.blocks.trigger_test) self.set_collision_test(world.blocks.collision_test) self.busy = False # used to block code execution during movement self.speaking = False self.speech_bubble = None self.python_interpreter = VirtualMachine(self) console_msg(name + " command interpreter initialised", 2) self.source_code = [] self.jets = [] # the particle streams that appear when flying # create 2 jets, 1 for each leg # the origin coordinates are just zero, # since they will be set each frame for j in range(2): self.jets.append( Jet( self.world, # link back to the game world (0, 0), # dummy start position (0, 1), # initial velocity vector )) self.wobble = [] # random drift when hovering rx = 4 ry = 2 for i in range(32): x = i / 2 - 8 # rx * math.cos(i*2*math.pi/32) y = ry * math.sin(i * 2 * math.pi / 32) - ry - 2 self.wobble.append((x, y)) for i in range(31, -1, -1): x = i / 2 - 8 # rx * math.cos(i*2*math.pi/32) y = -ry * math.sin(i * 2 * math.pi / 32) - ry - 2 self.wobble.append((0, 0)) # self.wobble.append((x, y)) self.wobble_counter = 0 self.take_off_animation = [] # take off animation sequence for i in range(32): self.take_off_animation.append((0, 0))
def clipboard_paste(self): """ paste the clipboard contents into the editor window currently this does not validate the clipboard contents It assumes text is encoded using UTF-8 and ignores all non-UTF-8 characters Text pasted from pycharm (and possibly IDLE too?) is encoded as HTML but this seems to paste ok for now """ console_msg("PASTE, 8") self.save_history() # strip trailing nulls clipboard_text = pygame.scrap.get(pygame.SCRAP_TEXT) \ .decode("utf-8", errors='ignore').replace('\0', '') for char in clipboard_text: # paste the chars in the keyboard, 1 at a time if char is chr(13): self.carriage_return(undo=False, pasting=True) elif chr(32) <= char <= chr(126): # only allow ASCII self.add_keystroke(char, undo=False)
def run_program(self): """ pass the text in the editor to the interpreter""" # run_enabled is set false on each run # and cleared using the reset button if self.python_interpreter.run_enabled: p = self.python_interpreter # for brevity p.load(self.get_source_code()) result, errors = p.compile() if result is False: # check for syntax errors # TODO display these using in-game dialogs if p.compile_time_error: error_msg = p.compile_time_error['error'] error_line = p.compile_time_error['line'] console_msg(self.name + ' SYNTAX ERROR:', 5) msg = error_msg + " on line " + str(error_line) console_msg(msg, 5) else: result, errors = p.run() # set the program going return result, errors return False, "RUN NOT ENABLED"
def check(self, character): # check if the flagpole has been activated trigger_rect = pygame.Rect(self.blocks[0].x, self.blocks[0].y, BLOCK_SIZE, BLOCK_SIZE) if (isinstance(character, Person) and not self.activated and character.location.colliderect(trigger_rect)): # unfurl the flag console_msg(self.name + " complete!", 1) # pass the level name to the save function self.world.complete_level(self.name) self.activated = True elif self.activated: # update the animation frame for the waving effect if self.flap_count > 0: self.frame_number = self.frame_number + .1 if self.frame_number >= self.frame_count: self.frame_number = 4.0 self.flap_count -= 1 f = int(self.frame_number) for b in self.blocks: b.image = b.frames[f]
def left_click(self): # check whether to click a button or reposition the cursor mouse_pos = (pygame.mouse.get_pos()[X], pygame.mouse.get_pos()[Y] - self.screen.get_size()[Y] + self.height) if self.surface.get_rect().collidepoint(mouse_pos): button_result = self.buttons.click(mouse_pos) if button_result is None: self.cursor_to_mouse_pos() # begin marking a selection at the current position self.selecting = True else: # clicking a button should never affect text selection self.selecting = False if button_result == button_tray.RUN: self.run_program() elif button_result == button_tray.STOP: console_msg("Execution halted.", 1) self.python_interpreter.halt() elif button_result == button_tray.LOAD: self.load_program() elif button_result == button_tray.SAVE: self.save_program() elif button_result == button_tray.CHANGE_COLOR: self.color_switch()
def convert_to_lines(self): """ convert the raw editor characters into lines of source code so that they can be saved/parsed etc conveniently""" console_msg("Converting to lines...", 8) source = [] line_number = 0 while line_number < len(self.text): # join the chars on this line into a single string # and remove trailing whitespace line = ''.join(self.text[line_number]).rstrip() # check for a continuation character (\) while line and line.rstrip()[-1] == '\\': line_number += 1 # remove continuation char and join lines line = line.rstrip('\\') + \ ''.join(self.text[line_number]).lstrip() console_msg("continuation line=" + line, 8) source.append(line) line_number += 1 console_msg("...done", 8) return source
def __init__(self, screen, bypass = False): """ displays the main menu and ensures that the user is logged in before proceeding. If bypass==True the menu creates a dummy session, used for testing.""" console_msg('Main menu', 0) self.screen = screen # this flag prevents certain key actions from automatically repeating # it is cleared when any key is released self.repeat_lock = False self._quit = False self._return_to_game = False self.clock = pygame.time.Clock() self.title_y_pos = 100 self.title_size = 28 self.items_y_pos = 370 self.title = "Main Menu" self.items = ["Play", "Options", "Quit:"] self.selected_item = -1 # start off with nothing selected self.session = None if bypass: self.session = Session("dummy_user", "dummy_class") self._return_to_game = True else: # load the fonts if pygame.font.get_init() is False: pygame.font.init() console_msg("Font system initialised", 2) # we explicitly load all required fonts # so that the TTF files can be bundled to run on other PCs self.menu_title_font = pygame.font.Font(MENU_FONT_FILE, 48) self.menu_title_bg_font = pygame.font.Font(MENU_FONT_FILE, 50) self.menu_font = pygame.font.Font(MENU_FONT_FILE, 32) self.menu_input_font = pygame.font.Font(CODE_FONT_FILE, 32) console_msg("Menu font loaded", 3) self.input_dialog = MenuInputDialog(self.screen, "Input", self.menu_input_font)
def __init__(self, screen, display, session): console_msg('Initialising world.', 0) self.screen = screen self.display = display self.session = session # load play/rewind icon images self.rewinding = False self.rewind_rotation = 0 self.rewind_icon = pygame.image.load(REWIND_ICON_FILE).convert() self.rewind_icon.set_colorkey((255, 255, 255), RLEACCEL) self.rewind_hover_icon = pygame.image.load( REWIND_HOVER_ICON_FILE).convert() self.rewind_hover_icon.set_colorkey((255, 255, 255), RLEACCEL) self.play_icon = pygame.image.load(PLAY_ICON_FILE).convert() self.play_icon.set_colorkey((255, 255, 255), RLEACCEL) self.play_hover_icon = pygame.image.load( PLAY_HOVER_ICON_FILE).convert() self.play_hover_icon.set_colorkey((255, 255, 255), RLEACCEL) self.play_disabled_icon = pygame.image.load( PLAY_DISABLED_ICON_FILE).convert() self.rewind_button_rect = pygame.Rect(REWIND_ICON_POS, (64, 64)) self.play_button_rect = pygame.Rect(PLAY_ICON_POS, (64, 64)) # when True the play button is displayed and programs can be run # once a program runs, this is set to false and the rewind button # is shown instead. self.run_enabled = True # load scenery layers self.scenery = scenery.Scenery(self.display, 'Day', 'Field') self.camera = Camera() # location of the game area on the window # used to scroll the game area out of the way of the code editor # this can't be done by the camera, because the editor is always just 'below' the visible part of the map # regardless of where the camera is currently panned to. In other words, the editor is not part of the # game world. self.game_origin = [0, 0] # load fonts if pygame.font.get_init() is False: pygame.font.init() console_msg("Font system initialised", 2) # we're not using the built-in SysFont any more # so that the TTF file can be bundled to run on other PCs self.code_font = pygame.font.Font(CODE_FONT_FILE, 18) console_msg("Deja Vu Sans Mono font loaded", 3) self.grid_font = pygame.font.Font(GRID_FONT_FILE, 8) console_msg("Pixel font loaded", 3) # load puzzle blocks self.blocks = blocks.BlockMap(self, BLOCK_TILE_DICTIONARY_FILE, BLOCK_TILESET_FILE, self.camera) console_msg("Map loaded", 1) self.puzzle = 0 # TODO: load current progress from log file self.session.set_current_level(self.blocks.get_puzzle_name( self.puzzle)) self.session.save_header() # initialise the environmental dust effect # DEBUG disabled due to looking bad # self.dust_storm = DustStorm(self) # load character sprites self.player = characters.Person(self, 'player', CHARACTER_SPRITE_FILE, (16, 20)) console_msg("player sprite initialised", 1) self.dog = characters.Robot( self, 'dog', DOG_SPRITE_FILE, (16, 16), ) console_msg("BIT sprite initialised", 1) self.player.set_position(self.blocks.get_player_start(self.puzzle)) self.dog.set_position(self.blocks.get_dog_start(self.puzzle)) self.dog.facing_right = False self.show_fps = False # this flag prevents certain key actions from automatically repeating # it is cleared when any key is released self.repeat_lock = False # intialise the python interpreter and editor if pygame.scrap.get_init() is False: pygame.scrap.init() console_msg("Clipboard initialised", 2) self.editor = code_editor.CodeWindow(screen, 300, self.code_font, self.dog, self.session) input_height = self.code_font.get_linesize() * 3 self.input = input_dialog.InputDialog(screen, input_height, self.code_font) console_msg("Editors initialised", 2) # load robot sentries for this level self.sentries = sentry.load_sentries(self, level=1) # load puzzles for this level # puzzle.load_puzzles(1, self) # initialise info signposts self.signposts = Signposts(self.code_font) console_msg("Info panels initialised", 7) self.playing = True # true when we are playing a level (not a menu) self.frame_draw_time = 1 self.frame_counter = 0 self.clock = pygame.time.Clock()
def check_keyboard_and_mouse(self): # input handling is moved here to avoid the main loop getting # too cluttered pressed = pygame.key.get_pressed() if pressed[K_a]: self.player.moving_left = True self.player.moving_right = False elif pressed[K_d]: self.player.moving_left = False self.player.moving_right = True # DEBUG give player the ability to fly! # elif pressed[K_w]: # self.player.moving_down = False; # self.player.moving_up = True # elif pressed[K_s]: # self.player.moving_down = True; # self.player.moving_up = False if pressed[K_w] or pressed[K_s]: if pressed[K_w]: self.camera.pan(-2) # self.camera_pan[Y] -= 2 else: self.camera.pan(2) # self.camera_pan[Y] += 2 elif not self.blocks.show_grid: self.camera.recentre() # self.camera_pan[Y] = int(self.camera_pan[Y] * 0.9) if pressed[K_ESCAPE]: # only show editor when it is completely hidden # this prevents it immediately reshowing after hiding if self.game_origin[Y] == 0: self.editor.show() # the number keys allow jumping directly to that puzzle # this is only enabled if the user has map editing privileges if ALLOW_MAP_EDITOR: level_shortcuts = [ K_0, K_1, K_2, K_3, K_4, K_5, K_6, K_7, K_8, K_9 ] for k in level_shortcuts: if pressed[k]: self.puzzle = level_shortcuts.index(k) self.rewind_level() if self.blocks.map_edit_mode: ctrl = pygame.key.get_mods() & KMOD_CTRL shift = pygame.key.get_mods() & KMOD_SHIFT # these actions do not auto repeat when held down if not self.repeat_lock: self.repeat_lock = True if pressed[K_F9]: console_msg("Saving map...", 1, line_end='') self.blocks.save_grid() console_msg("done", 1) elif pressed[K_RIGHT]: self.blocks.cursor_right() elif pressed[K_LEFT]: self.blocks.cursor_left() elif pressed[K_UP]: if ctrl: self.camera.pan(-BLOCK_SIZE) else: self.blocks.cursor_up() elif pressed[K_DOWN]: if ctrl: self.camera.pan(BLOCK_SIZE) else: self.blocks.cursor_down() elif pressed[K_LEFTBRACKET]: # [ # not used, now we have a block palette on-screen # self.blocks.previous_editor_tile() pass elif pressed[K_RIGHTBRACKET]: # ] # self.blocks.next_editor_tile() pass elif pressed[K_BACKSPACE]: if self.blocks.mover_is_selected(): console_msg("deleting mover", 8) self.blocks.remove_moveable_group() elif self.blocks.trigger_is_selected(): console_msg("deleting trigger", 8) self.blocks.remove_trigger() else: self.blocks.blank_editor_tile() elif pressed[K_RETURN]: # change/add a block # at the current grid cursor location self.blocks.change_block() elif pressed[K_TAB]: # switch between midground and foreground block layers self.blocks.switch_layer() elif pressed[K_r]: # reset all block triggers and movers self.blocks.reset() elif pressed[K_h]: # home the cursor to the centre of the screen self.blocks.home_cursor() elif pressed[K_m]: # turn the selection into a movable group self.blocks.create_mover() elif pressed[K_t]: # turn the block at the cursor into a trigger # or link an existing trigger to a mover self.blocks.set_trigger() elif pressed[K_l]: # toggle random mode for the trigger actions # if the cursor is currently on a trigger self.blocks.toggle_trigger_randomness() elif pressed[K_INSERT]: # insert a new column of blocks at the cursor self.blocks.insert_column() elif pressed[K_DELETE]: # remove a column at the cursor self.blocks.delete_column() else: self.repeat_lock = False # reset, since no key pressed for event in pygame.event.get(): if event.type == pygame.MOUSEBUTTONDOWN: mouse_pos = ( pygame.mouse.get_pos()[X] / SCALING_FACTOR + self.camera.scroll_x(), pygame.mouse.get_pos()[Y] / SCALING_FACTOR + self.camera.scroll_y()) if event.button == 1: # left click if shift: self.blocks.select_block(mouse_pos, 'add') else: # just select a single block self.blocks.select_block(mouse_pos, 'set') elif event.button == 3: # right click self.blocks.select_block(mouse_pos, 'pick') # 2: middle button # 4: scroll up # 5: scroll down # DEBUG stats if pressed[K_f]: if not self.repeat_lock: # toggle fps stats self.show_fps = not self.show_fps if not self.show_fps: self.frame_counter = 0 if pressed[K_g]: ctrl = pygame.key.get_mods() & KMOD_CTRL shift = pygame.key.get_mods() & KMOD_SHIFT if not self.repeat_lock: if ALLOW_MAP_EDITOR and ctrl and shift: self.blocks.toggle_map_editor() else: self.blocks.toggle_grid() self.repeat_lock = True # check the mouse to see if any buttons were clicked # currently just the rewind and play button self.check_buttons() # check if any signposts or info panels were clicked # button 0 is left click if pygame.mouse.get_pressed(num_buttons=5)[0]: self.blocks.signposts.check_signpost_at(pygame.mouse.get_pos(), self.camera.scroll())
def stop_selecting(self): self.selecting = False console_msg("End selection", 8)
def start_selecting(self): self.selecting = True self.selection_start = (self.cursor_col, self.cursor_line) console_msg("Begin selection", 8)
def load_puzzles(level, world): # load all the puzzles for a given level from the file file_name = PUZZLE_FILE with open(file_name, 'r') as file: lines = file.readlines() i = 0 # look for the start of a puzzle while i < len(lines) and lines[i] != PUZZLE_START: i += 1 if i < len(lines): # get level number i += 1 level = eval(lines[i]) # get puzzle name i += 1 name = lines[i][:-1] # strip the trailing CR+LF i += 1 # get location of the sentry that will host the puzzle sentry_position = eval(lines[i]) sentry = Sentry(sentry_position) # get the instructions for the puzzle i += 1 instructions = convert_to_f_string(lines[i][:-1]) # get the puzzle data (parameters for the puzzle) i += 1 data = [] # data is stored as tuples representing the range of possible values # for each item. If there are exactly 2 numeric values in the tuple, # it is interpreted as the lower and upper bounds for randint() # otherwise it is treated as a list for choice() data_strings = eval(lines[i]) # the list of tuples for all the parameters for item in data_strings: if len(item) == 2 and isinstance(item[0], int) and isinstance(item[1], int): data.append(random.randint(int(item[0]), int(item[1]))) else: data.append(random.choice(item)) print(eval(instructions)) # DEBUG will this pick up the f string param? # get the source code for the exemplar solution i += 1 print(data) if lines[i] == SOLUTION_START: i += 1 solution_source = [] while lines[i] != SOLUTION_END and i < len(lines): solution_source.append(eval(convert_to_f_string(lines[i][:-1]))) i += 1 print(solution_source) self.run_program(solution_source) if i >= len(lines): console_msg("Error: missing end_of_solution in puzzle file!", 0) else: console_msg("Error: missing start_of_solution in puzzle file!", 0) else: # ran off the end of the file console_msg('Error parsing puzzle file!', 0) def run_program(self, source): """ pass the text in the puzzle solution to the interpreter""" p = interpreter.VirtualMachine(self.robot) p.load(source) result, errors = p.compile() if result is False: # check for syntax errors # TODO display these using in-game dialogs if p.compile_time_error: error_msg = p.compile_time_error['error'] error_line = p.compile_time_error['line'] console_msg('BIT found a SYNTAX ERROR:', 5) msg = error_msg + " on line " + str(error_line) console_msg(msg, 5) else: result, errors = p.run() # set the program going
def clipboard_copy(self): console_msg("COPY", 8)
def clipboard_cut(self): console_msg("CUT", 8) self.save_history()
def __init__(self, input_combo, action): self.combo = input_combo # string representing the event eg CTRL+S or ESCAPE or LEFT_CLICK self.modifier_keys = get_modifier_list(input_combo) # list of pygame KMOD_xxx values eg KMOD_SHIFT self.action = action # the function that is called when the input event is detected console_msg("Registering event for " + self.combo + " mods=" + str(self.modifier_keys), 9)
def update(self, focus): """update all the game world stuff focus is the character that the camera follows This is the dog when a program is running on BIT, otherwise the player""" display = self.display # for brevity frame_start_time = time.time_ns() # used to calculate fps # track the camera with the focus character, but with a bit of lag self.camera.update(focus) # render the background # OLD RENDER METHOD: self.scenery.draw_background(display, self.camera.scroll()) self.scenery.draw(self.camera.scroll()) # draw the 'midground' blocks behind the characters self.blocks.update_midground(display, self.camera.scroll()) # move and render the player sprite self.player.update(display, self.camera.scroll()) # move and render the dog self.dog.update(display, self.camera.scroll()) # draw all the robot sentries for s in self.sentries: s.update(display, self.camera.scroll()) # draw the 'foreground' blocks in front of the characters # this is just foliage and other cosmetic stuff self.blocks.update_foreground(display, self.camera.scroll()) # update the input window and editor, if necessary # the input window takes precedence if both are open if self.input.is_active(): self.input.update() elif self.editor.is_active(): self.editor.update() # still need to check if buttons outside the editor were clicked self.check_buttons() else: # only handle keystrokes for game control # if the code editor isn't open self.check_keyboard_and_mouse() # process all other events to clear the queue for event in pygame.event.get(): if event.type == KEYUP: self.repeat_lock = False # release the lock if event.type == QUIT: self.playing = False if self.show_fps: self.frame_counter += 1 if self.frame_counter > 60: self.frame_counter = 0 console_msg( 'frame draw:{0}ms fps:{1} render budget left:{2}ms'.format( self.frame_draw_time / 1000000, int(1000000000 / self.frame_draw_time), int((1000000000 - 60 * self.frame_draw_time) / 1000000)), 1) # scroll the editor in and out of view as required if self.editor.is_active(): if self.game_origin[Y] > -self.editor.height: self.game_origin[Y] -= EDITOR_POPUP_SPEED self.editor.draw() elif self.game_origin[Y] < 0: self.game_origin[Y] += EDITOR_POPUP_SPEED # scale the rendering area to the actual game window self.screen.blit(pygame.transform.scale(display, WINDOW_SIZE), self.game_origin) # the input window and code editor sit below the game surface # ie at a higher Y value, not below in the sense of a different layer editor_position = (self.game_origin[X], self.game_origin[Y] + WINDOW_SIZE[Y]) self.screen.blit(self.editor.surface, editor_position) # draw the input window, if it is currently active if self.input.is_active(): self.input.draw() input_dialog_position = (self.game_origin[X], self.game_origin[Y] + WINDOW_SIZE[Y] - self.input.height) self.screen.blit(self.input.surface, input_dialog_position) # overlay any speech bubbles and info windows at the native resolution if self.dog.speaking: position = self.dog.speech_position() position[X] = (position[X] - self.camera.scroll_x() ) * SCALING_FACTOR + self.game_origin[X] position[Y] = (position[Y] - self.camera.scroll_y() ) * SCALING_FACTOR + self.game_origin[Y] self.screen.blit(self.dog.get_speech_bubble(), position) # do the same for any sentries for s in self.sentries: if s.speaking: position = s.speech_position() position[X] = (position[X] - self.camera.scroll_x() ) * SCALING_FACTOR + self.game_origin[X] position[Y] = (position[Y] - self.camera.scroll_y() ) * SCALING_FACTOR + self.game_origin[Y] self.screen.blit(s.get_speech_bubble(), position) self.blocks.signposts.update_open_signs(self.screen, self.camera.scroll(), self.game_origin) # draw the swirling dust - DEBUG disabled due to looking bad # self.dust_storm.update(self.screen, self.game_origin[Y], scroll) # draw the grid overlay next so it is on top of all blocks if self.blocks.show_grid: self.blocks.draw_grid(self.screen, self.game_origin) # previously, the grid took the colour from the editor choice # self.editor.get_fg_color()) # draw the map editor info panel and block palette if self.blocks.map_edit_mode: self.blocks.draw_edit_info_box(self.screen) # self.blocks.draw_block_palette(self.screen, self.game_origin) # draw the rewind button in the top right corner if self.rewinding: # update the rotation animation self.rewind_rotation = (self.rewind_rotation + 10) if self.rewind_rotation >= 360: self.rewind_rotation = 0 self.rewinding = False rewind_animation_icon = pygame.transform.rotate( self.rewind_hover_icon, self.rewind_rotation) icon_size = rewind_animation_icon.get_size() self.screen.blit(rewind_animation_icon, (REWIND_ICON_POS[X] + 32 - icon_size[X] / 2, REWIND_ICON_POS[Y] + 32 - icon_size[Y] / 2)) else: if self.rewind_button_rect.collidepoint(pygame.mouse.get_pos()): self.screen.blit(self.rewind_hover_icon, REWIND_ICON_POS) else: self.screen.blit(self.rewind_icon, REWIND_ICON_POS, special_flags=BLEND_RGB_MULT) # play button if self.dog.get_interpreter().run_enabled: if self.play_button_rect.collidepoint(pygame.mouse.get_pos()): self.screen.blit(self.play_hover_icon, PLAY_ICON_POS) else: self.screen.blit(self.play_icon, PLAY_ICON_POS, special_flags=BLEND_RGB_MULT) else: self.screen.blit(self.play_disabled_icon, PLAY_ICON_POS, special_flags=BLEND_RGB_MULT) # TODO self.end_of_level_display() pygame.display.update() # actually display self.frame_draw_time = time.time_ns() - frame_start_time self.clock.tick(60) # lock the framerate to 60fps