def wander(self): self.set_home_pos() self._generate_direction() self._pause_wander = False self._wander_timer = Timer(ms=self.MOVE_INTERVAL_MS, cb=self._on_wander_tick)
def start_animation(self, name): self.frame_state = name self.anim_frame = 0 if self.anim_timer: self.anim_timer.stop() self.anim_timer = Timer(ms=self.ANIM_MS, cb=self._on_anim_tick)
class Wife(ChaseMixin, Human, WalkingSprite): MOVE_SPEED = 1 CHASE_SPEED = 1 WANDER_KEY_NAME = 'walking' DEFAULT_HEALTH = 1 CLEAN_NAME = 'wife' INFECTED_NAME = 'infectedwife' NAME = INFECTED_NAME TRANSITION_MS = 250 MAX_TRANSITIONS = 6 def __init__(self): super(Wife, self).__init__() self.transitioned = Signal() self.showed_exclamation = False def on_collision(self, dx, dy, obj, self_rect, obj_rect): if obj == get_engine().player and not self.showed_exclamation: get_engine().ui.close_monologues() self.show_exclamation('heart_exclamation', self._transition_clean) self.showed_exclamation = True def _transition_clean(self): self.transition_count = 0 self.transition_timer = Timer(ms=self.TRANSITION_MS, cb=lambda: self._on_transition()) def _on_transition(self): self.transition_count += 1 if self.name == self.INFECTED_NAME: self.name = self.CLEAN_NAME else: self.name = self.INFECTED_NAME if self.transition_count == self.MAX_TRANSITIONS: self.name = self.CLEAN_NAME self.transition_timer.stop() self.transitioned.emit()
def __init__(self): super(Player, self).__init__('player') # Signals self.health_changed = Signal() self.lives_changed = Signal() # State self.human_kill_count = 0 self.shoot_timer = Timer(ms=self.SHOOT_MS, cb=self.shoot, start_automatically=False) self.reset()
def show_monologue(self, text, timeout_ms=None, on_done=None, y_offset=MONOLOGUE_Y_OFFSET, actor=None, **kwargs): def _next_monologue(): self.close(self.active_monologue) if len(text) > 1: self.show_monologue(text[1:], timeout_ms, on_done, **kwargs) elif on_done: on_done() if not isinstance(text, list): text = [text] self.close_monologues() if actor is None: actor = self.engine.player clip_rect = self.engine.camera.rect offset = (-clip_rect.x, -clip_rect.y) lines = text[0].splitlines() textbox = TextBox(self, lines, stay_open=True, **kwargs) self.widgets.append(textbox) textbox.rect = pygame.Rect( self.MONOLOGUE_X, actor.rect.move(offset).bottom + y_offset, self.size[0] - 2 * self.PADDING - self.MONOLOGUE_X, self.MONOLOGUE_HEIGHT * len(lines)) self.active_monologue = textbox self.monologue_timer = Timer(ms=timeout_ms or self.MONOLOGUE_TIMEOUT_MS, cb=_next_monologue, one_shot=True) return textbox
def blink(self, ms, on_done): self.blink_count = 0 self.visible = 0 self.blink_timer = Timer(ms=ms, cb=lambda: self._on_blinked(on_done))
def start(self): self.anim_timer = Timer(ms=self.ANIM_MS, cb=self._on_anim_tick, start_automatically=False) self.started = True
class Sprite(BaseSprite): NAME = None MOVE_SPEED = 4 RUN_SPEED = 8 ANIM_MS = 150 DEATH_BLINK_MS = 250 MAX_BLINKS = 4 LETHAL = False SPRITESHEET_ROWS = 1 SPRITESHEET_COLS = 1 SPRITESHEET_FRAMES = { Direction.SOUTH: { 'default': [(0, 0)], }, Direction.WEST: { 'default': [(0, 0)], }, Direction.EAST: { 'default': [(0, 0)], }, Direction.NORTH: { 'default': [(0, 0)], }, } NEED_TICKS = True def __init__(self, name=None): super(Sprite, self).__init__() # Signals self.moved = Signal() self.dead = Signal() # State self.quad_trees = set() self.layer = None self.name = name or self.NAME assert self.name self.started = False self.direction = Direction.SOUTH self.velocity = (0, 0) self.speed = self.MOVE_SPEED self.can_move = True self.use_quadtrees = True self.autoset_velocity = True self.frame_state = 'default' self.anim_frame = 0 self.anim_timer = None def start(self): self.anim_timer = Timer(ms=self.ANIM_MS, cb=self._on_anim_tick, start_automatically=False) self.started = True def stop(self): self.stop_moving() self.started = False def damage(self, damage_value): if self.health > 0: self.health -= damage_value if self.health <= 0: self.die() return True return False def die(self, on_done=None): self.stop() self.blink(self.DEATH_BLINK_MS, lambda: self._stop_and_die(on_done)) def _stop_and_die(self, on_done): self.dead.emit() self.remove() if on_done: on_done() def blink(self, ms, on_done): self.blink_count = 0 self.visible = 0 self.blink_timer = Timer(ms=ms, cb=lambda: self._on_blinked(on_done)) def _on_blinked(self, on_done): self.blink_count += 1 if self.visible: self.visible = 0 else: self.visible = 1 if self.blink_count == self.MAX_BLINKS: self.visible = 1 self.blink_timer.stop() if on_done: on_done() def remove(self): self.stop() self.collidable = False self.visible = 0 self.dirty = 1 self.layer.remove(self) self.layer = None def update_image(self): self.image = self.generate_image() assert self.image self.rect.size = self.image.get_size() self.update_collision_rects() def generate_image(self): return load_spritesheet_frame( self.name, self._get_spritesheet_frames()[self.anim_frame], self.SPRITESHEET_ROWS, self.SPRITESHEET_COLS) def _get_spritesheet_frames(self): return self.SPRITESHEET_FRAMES[self.direction][self.frame_state] def move_by(self, dx, dy, check_collisions=True): super(Sprite, self).move_by(dx, dy, check_collisions=check_collisions) self.moved.emit(dx, dy) def set_direction(self, direction): if self.direction != direction: self.direction = direction self.update_velocity() self.update_image() def start_animation(self, name): self.frame_state = name self.anim_frame = 0 if self.anim_timer: self.anim_timer.stop() self.anim_timer = Timer(ms=self.ANIM_MS, cb=self._on_anim_tick) def stop_moving(self): self.velocity = (0, 0) self.frame_state = 'default' self.anim_frame = 0 if self.anim_timer: self.anim_timer.stop() self.anim_timer = None def recompute_direction(self): if abs(self.velocity[0]) > abs(self.velocity[1]): if self.velocity[0] > 0: self.set_direction(Direction.EAST) elif self.velocity[0] < 0: self.set_direction(Direction.WEST) elif abs(self.velocity[1]) >= abs(self.velocity[0]): if self.velocity[1] > 0: self.set_direction(Direction.SOUTH) elif self.velocity[1] < 0: self.set_direction(Direction.NORTH) def update_collision_rects(self): pass def update_velocity(self): if not self.started or not self.autoset_velocity: return x, y = { Direction.WEST: (-1, None), Direction.EAST: (1, None), Direction.NORTH: (None, -1), Direction.SOUTH: (None, 1), }[self.direction] if x: x *= self.speed else: x = self.velocity[0] if y: y *= self.speed else: y = self.velocity[1] self.velocity = (x, y) def tick(self): if self.started and self.velocity != (0, 0): self.move_by(*self.velocity) def _on_anim_tick(self): frames = self._get_spritesheet_frames() self.anim_frame += 1 if self.anim_frame == len(frames): self.anim_frame = 0 self.dirty = 2 self.update_image()
class Player(WalkingSprite): MAX_LIVES = 3 MAX_HEALTH = 8 SHOOT_MS = 500 FALL_SPEED = 10 HURT_BLINK_MS = 250 SPRITESHEET_COLS = 4 SPRITESHEET_FRAMES = { Direction.SOUTH: dict( WalkingSprite.SPRITESHEET_FRAMES[Direction.SOUTH], **{ 'shooting': [(3, 0)], 'falling': [(1, 0)], }), Direction.WEST: dict( WalkingSprite.SPRITESHEET_FRAMES[Direction.WEST], **{ 'shooting': [(3, 1)], 'falling': [(1, 1)], }), Direction.EAST: dict( WalkingSprite.SPRITESHEET_FRAMES[Direction.EAST], **{ 'shooting': [(3, 2)], 'falling': [(1, 2)], }), Direction.NORTH: dict( WalkingSprite.SPRITESHEET_FRAMES[Direction.NORTH], **{ 'shooting': [(3, 3)], 'falling': [(1, 3)], }), } def __init__(self): super(Player, self).__init__('player') # Signals self.health_changed = Signal() self.lives_changed = Signal() # State self.human_kill_count = 0 self.shoot_timer = Timer(ms=self.SHOOT_MS, cb=self.shoot, start_automatically=False) self.reset() def update_collision_rects(self): self.collision_rects = [ pygame.Rect(0, self.rect.height / 2, self.rect.width, self.rect.height / 2), ] def reset(self): self.health = self.MAX_HEALTH self.lives = self.MAX_LIVES self.invulnerable = False self.running = False self.falling = False self.shooting = False self.can_run = True self.collidable = True self.allow_player_control = True self.health_changed.emit() self.lives_changed.emit() self.set_running(True) self.stop_moving() self._update_animation() def handle_event(self, event): if not self.allow_player_control: return if event.type == KEYDOWN: if event.key == K_RIGHT: self.move_direction(Direction.EAST) elif event.key == K_LEFT: self.move_direction(Direction.WEST) elif event.key == K_UP: self.move_direction(Direction.NORTH) elif event.key == K_DOWN: self.move_direction(Direction.SOUTH) elif event.key == K_c: self.set_shooting(True) elif event.key == K_F4: self.collidable = not self.collidable elif event.type == KEYUP: if event.key == K_RIGHT: self.stop_moving_direction(Direction.EAST) elif event.key == K_LEFT: self.stop_moving_direction(Direction.WEST) elif event.key == K_UP: self.stop_moving_direction(Direction.NORTH) elif event.key == K_DOWN: self.stop_moving_direction(Direction.SOUTH) elif event.key == K_c: self.set_shooting(False) def move_direction(self, direction): self.direction = direction self.update_velocity() self._update_animation() self.update_image() def stop_moving_direction(self, direction): if direction in (Direction.WEST, Direction.EAST): self.velocity = (0, self.velocity[1]) elif direction in (Direction.NORTH, Direction.SOUTH): self.velocity = (self.velocity[0], 0) # The direction may not make any sense anymore, so recompute it. self.recompute_direction() self._update_animation() def set_shooting(self, shooting): if self.shooting == shooting: return self.shooting = shooting if shooting: self.shoot_timer.start() self.shoot() else: self.shoot_timer.stop() self._update_animation() def shoot(self): bullet = Bullet(self) self.layer.add(bullet) bullet.move_beside(self, self.direction) bullet.start() bullet.set_direction(self.direction) bullet.update_velocity() def should_adjust_position_with(self, obj, dx, dy): return not isinstance(obj, Bullet) or obj.owner_sprite != self def set_running(self, running): self.running = running if running: self.speed = self.RUN_SPEED else: self.speed = self.MOVE_SPEED if self.velocity != (0, 0): self.update_velocity() self._update_animation() def stop_running(self): self.running = False self._update_animation() def fall(self): self.collidable = False self.stop_running() self.set_direction(Direction.SOUTH) self.velocity = (0, self.FALL_SPEED) self.falling = True self._update_animation() def _update_animation(self): if self.velocity == (0, 0): if self.shooting and self.frame_state != 'shooting': self.frame_state = 'shooting' elif not self.shooting and self.frame_state != 'default': self.frame_state = 'default' else: return self.anim_timer.stop() else: new_state = None if self.running and self.frame_state != 'running': new_state = 'running' elif self.falling: new_state = 'falling' elif self.frame_state != 'walking': new_state = 'walking' else: return if new_state: self.start_animation(new_state) self.anim_frame = 0 self.update_image() def on_collision(self, dx, dy, obj, self_rect, obj_rect): if obj.LETHAL and self.health > 0: if not self.invulnerable: self.health -= 1 self.health_changed.emit() if self.health == 0: self.on_dead() else: self.invulnerable = True self.blink(self.HURT_BLINK_MS, self._on_hurt_blink_done) return True def _on_hurt_blink_done(self): self.invulnerable = False def on_dead(self): self.lives -= 1 self.lives_changed.emit() self.stop_moving() engine = get_engine() if self.lives == 0: self.die(engine.game_over) else: self.health = self.MAX_HEALTH self.health_changed.emit() engine.dead()
def _transition_clean(self): self.transition_count = 0 self.transition_timer = Timer(ms=self.TRANSITION_MS, cb=lambda: self._on_transition())
class WanderMixin(object): MOVE_INTERVAL_MS = 1000 PAUSE_CHANCE = 0.1 CHANGE_DIR_CHANCE = 0.3 WANDER_KEY_NAME = 'wandering' WANDER_DISTANCE = 64 * 8 def __init__(self, *args, **kwargs): super(WanderMixin, self).__init__(*args, **kwargs) self.auto_wander = True self._wander_timer = None def start(self): super(WanderMixin, self).start() if self.auto_wander: self.wander() def stop(self): super(WanderMixin, self).stop() self.stop_wandering() def wander(self): self.set_home_pos() self._generate_direction() self._pause_wander = False self._wander_timer = Timer(ms=self.MOVE_INTERVAL_MS, cb=self._on_wander_tick) def stop_wandering(self): if self._wander_timer: self._wander_timer.stop() self._wander_timer = None def set_home_pos(self): self.home_pos = self.rect.center def _generate_direction(self): self.velocity = (0, 0) # Do this twice, so we can maybe get different X, Y velocities. self.set_direction(random.randint(0, 3)) self.update_velocity() self.set_direction(random.randint(0, 3)) self.update_velocity() def _on_wander_tick(self): if self._pause_wander: self._pause_wander = False return dist_x = self.home_pos[0] - self.rect.centerx dist_y = self.home_pos[1] - self.rect.centery turning_back = False if abs(dist_x) >= self.WANDER_DISTANCE / 2: self.velocity = (-self.velocity[0], self.velocity[1]) turning_back = True if abs(dist_y) >= self.WANDER_DISTANCE / 2: self.velocity = (self.velocity[0], -self.velocity[1]) turning_back = True if turning_back: self.recompute_direction() else: if random.random() <= self.CHANGE_DIR_CHANCE: self._generate_direction() if random.random() <= self.PAUSE_CHANCE: self._pause_wander = True self.stop_moving() elif self.frame_state != self.WANDER_KEY_NAME: self.start_animation(self.WANDER_KEY_NAME)
class GameUI(object): PADDING = 40 TEXTBOX_HEIGHT = 150 STATUS_AREA_HEIGHT = 40 MONOLOGUE_X = 250 MONOLOGUE_Y_OFFSET = 50 MONOLOGUE_HEIGHT = 50 MONOLOGUE_TIMEOUT_MS = 3000 def __init__(self, engine): pygame.font.init() self.ready = Signal() self.engine = engine self.size = engine.screen.get_size() self.surface = pygame.Surface(self.size).convert_alpha() self.widgets = [] self.timers = [] self.active_monologue = None self.monologue_timer = None self.paused_textbox = None self.confirm_quit_box = None self.default_font_file = get_font_filename() self.font = pygame.font.Font(self.default_font_file, 20) self.small_font = pygame.font.Font(self.default_font_file, 16) self.status_area = StatusArea(self) self.widgets.append(self.status_area) def show_textbox(self, text, **kwargs): textbox = TextBox(self, text, **kwargs) textbox.rect = pygame.Rect( self.PADDING, (self.size[1] - self.TEXTBOX_HEIGHT) / 2, self.size[0] - 2 * self.PADDING, self.TEXTBOX_HEIGHT) self.widgets.append(textbox) return textbox def show_monologue(self, text, timeout_ms=None, on_done=None, y_offset=MONOLOGUE_Y_OFFSET, actor=None, **kwargs): def _next_monologue(): self.close(self.active_monologue) if len(text) > 1: self.show_monologue(text[1:], timeout_ms, on_done, **kwargs) elif on_done: on_done() if not isinstance(text, list): text = [text] self.close_monologues() if actor is None: actor = self.engine.player clip_rect = self.engine.camera.rect offset = (-clip_rect.x, -clip_rect.y) lines = text[0].splitlines() textbox = TextBox(self, lines, stay_open=True, **kwargs) self.widgets.append(textbox) textbox.rect = pygame.Rect( self.MONOLOGUE_X, actor.rect.move(offset).bottom + y_offset, self.size[0] - 2 * self.PADDING - self.MONOLOGUE_X, self.MONOLOGUE_HEIGHT * len(lines)) self.active_monologue = textbox self.monologue_timer = Timer(ms=timeout_ms or self.MONOLOGUE_TIMEOUT_MS, cb=_next_monologue, one_shot=True) return textbox def close_monologues(self): if self.active_monologue: self.monologue_timer.stop() self.close(self.active_monologue) def show_dialogue(self, actors, lines, timeout_ms=None, on_done=None, **kwargs): def _next_dialogue(): self.close(self.active_monologue) if len(lines) > 1: self.show_dialogue(actors, lines[1:], timeout_ms, on_done, **kwargs) elif on_done: on_done() person, text = lines[0] if person == 'player': self.show_monologue(text, timeout_ms, _next_dialogue) else: self.show_monologue(text, timeout_ms, _next_dialogue, actor=actors[person], y_offset=-3.5 * self.MONOLOGUE_Y_OFFSET, bg_color=(232, 174, 174), text_color=(0, 0, 0), border_color=(0, 0, 0)) def close(self, widget): try: self.widgets.remove(widget) widget.closed.emit() except ValueError: # It was already closed pass def handle_event(self, event): handled = False if event.type == KEYDOWN: if self.confirm_quit_box: handled = True if event.key in (K_ESCAPE, K_n): self.confirm_quit_box.close() self.confirm_quit_box = None self.engine.paused = False elif event.key == K_y: self.engine.quit() elif event.key in (K_ESCAPE, K_RIGHT, K_SPACE, K_RETURN): for widget in self.widgets: if (isinstance(widget, TextBox) and widget != self.paused_textbox and not widget.stay_open): widget.close() handled = True return handled def pause(self): assert not self.paused_textbox self.paused_textbox = self.show_textbox('Paused') def unpause(self): if self.paused_textbox: self.paused_textbox.close() self.paused_textbox = None def confirm_quit(self): if self.confirm_quit_box: self.confirm_quit_box.close() return self.engine.paused = True self.confirm_quit_box = self.show_textbox([ "Giving up already?", "'Y' to give up.", "'N' to keep playing." ]) def show_opening_scene(self, on_done): lines = [ (3000, "I'm Dr. Nick Rogers, Ph.D."), (6000, "I moved to this little town in the mountains with my " "wife, Laura, just 6 short months ago.\n" "I wanted a safe and quiet place to conduct my research."), (3000, "I planned to cure the common cold."), (6000, "The plan was to create an airborne virus, safe for " "humans, that would hunt down and destroy\n" "the viruses that cause the cold symptoms."), (6000, "My experiments on rats and chimps were promising.\n" "I needed to begin human trials, but that would take " "years."), (6000, "This morning... The details are still a bit fuzzy. " "I woke up in my lab up\n" "against the wall with a terrible headache."), (3000, "There was an explosion in the lab, and the virus was exposed."), (4000, "Stumbling out of the lab, I saw the townfolk bleeding, " "screaming, faces distorted."), (7000, "I had seen this before. In one of the batches of the " "virus, the chimps began to hallucinate. \n" "Some became aggressive and twisted their faces up in " "agony."), (6000, "I appeared to be fine, and I knew how to cure this. " "Unfortunately, the ingredients were destroyed."), (4000, "I told Laura to go to the mountains where it was safe, and " "ventured out to find the ingredients for the cure."), ] self._show_opening_line(lines, on_done) def _show_opening_line(self, lines, on_done, prev_textbox=None): if prev_textbox: self.close(prev_textbox) if not lines: on_done() else: timeout, text = lines[0] textbox = self.show_textbox(text.split('\n')) textbox.closed.connect( lambda: self._show_opening_line(lines[1:], on_done, textbox)) Timer(ms=timeout, cb=textbox.close, one_shot=True) def draw(self, surface): for element in self.widgets: element.draw(surface)