class SelectedSpellBoxWidget(AbstractUIWidget): box_pos = config_ui["selected_spell_box_pos"] box_size = config_ui["selected_spell_box_size"] icon_move = config_ui["selected_spell_box_icon_move"] icon_pos = (box_pos[0] + icon_move[0], box_pos[1] + icon_move[1]) icon_size = config_ui["selected_spell_box_icon_size"] box_image = imglib.load_image_from_file( "images/sl/ui/SelectedSpellBox.png", after_scale=box_size) def __init__(self, game, player): super().__init__(game, player) self.display_icon = self.last_selected = self.last_icon = None self.update() def update(self): select = self.player.selected_spell if self.player.selected_spell is not self.last_selected: self.last_selected = select if select is not None: self.display_icon = imglib.scale(select.icon, self.icon_size, docache=False) else: self.display_icon = None def draw(self, screen): screen.blit(self.box_image, self.box_pos) if self.display_icon is not None: screen.blit(self.display_icon, self.icon_pos)
class Embers(AbstractSpell): icon = imglib.load_image_from_file("images/pt/fire-arrows-2.png", after_scale=base_icon_size) tree_pos = (0, 0) # Position from center mana_cost = 10 count_min, count_max = 1, 3 angle_spread = 10 mul_if_moving = 2 @requires_mana(mana_cost) def cast(self): level = self.player.level mid_angle = self.player.best_heading_vector.to_angle() level = self.player.level pos = self.player.rect.center count = random.randint(self.count_min, self.count_max) any_move = any(self.player.moving.values()) for i in range(count): vec = utils.Vector.from_angle( mid_angle + random.randint(-self.angle_spread, self.angle_spread)) projectile = projectiles.Ember(level, pos, norm_vector=vec) if any_move: projectile.dist *= self.mul_if_moving projectile.set_ease_args() level.sprites.append(projectile)
class Rune(BaseSprite): friendly = True hostile = False cachable = False size = (20, 20) damage = 0.5 aoe = 50 damage_aoe = 0.25 surface = imglib.load_image_from_file( "images/sl/spells/RunePoison.png", after_scale=size) def __init__(self, level, pos): super().__init__() self.level = level self.rect = pygame.Rect((0, 0), self.size) self.rect.center = pos def update(self): if self.simple_deal_damage(): self.level.sprites.remove(self) for i in range(random.randint(4, 7)): p = particles.Particle.from_sprite(self, 4, utils.Vector.uniform(3), 50, Color.Green) self.level.particles.append(p) for sprite in self.level.hostile_sprites: if utils.dist(self.rect.center, sprite.rect.center) < self.aoe: sprite.take_damage(self.damage_aoe)
class DoorTile(BaseTile): needs_update = False passable = True transparent = False flags_template = FlagSet(TileFlags.Passage) drawn_surface = imglib.load_image_from_file("images/dd/env/DoorOnWall.png", after_scale=tile_size_t)
class Arrow(OmniProjectile): rotating = True hostile = True friendly = False base_size = (15, 3) base_image = imglib.load_image_from_file("images/sl/projectiles/Arrow.png", after_scale=base_size) speed = 10 damage = 0.5
class GrayGoo(BaseEnemy): base_move_speed = 2 base_damage = 0.5 base_max_health_points = 1 damage_on_player_touch = True size = (30, 30) surface = imglib.load_image_from_file("images/dd/enemies/GrayGoo.png", after_scale=size) def __init__(self, level, spawner_tile): super().__init__(level, spawner_tile) self.ticks_to_wait = 0 self.moving = {k: False for k in base_directions} self.set_random_move_direction() def update(self): super().update() last_rect = self.rect if not self.ticks_to_wait: self.moving[self.direction] = True self.handle_moving() if self.rect == last_rect: self.set_random_move_direction() else: self.ticks_to_wait -= 1 def set_random_move_direction(self): possible_directions = [] col, row = self.closest_tile_index level_size = self.level.layout_size if col > 0 and self.level.layout[row][col - 1].passable: possible_directions.append("left") if col < level_size[0] - 1 and self.level.layout[row][col + 1].passable: possible_directions.append("right") if row > 0 and self.level.layout[row - 1][col].passable: possible_directions.append("up") if row < level_size[1] - 1 and self.level.layout[row + 1][col].passable: possible_directions.append("down") if not possible_directions: possible_directions = ["left", "right", "up", "down"] self.direction = random.choice(possible_directions) def create_cache(self): cache = super().create_cache() cache.update({"direction": self.direction}) return cache @classmethod def from_cache(cls, level, spawner_tile, cache): obj = super().from_cache(level, spawner_tile, cache) obj.direction = cache["direction"] return obj
def __init__(self, level, pos, *, centerpos=True, norm_vector): super().__init__(level, pos, centerpos=centerpos, norm_vector=norm_vector) anim_size = (self.base_size[0] * self.animation_frames, self.base_size[1]) animation_surf = imglib.load_image_from_file( "images/sl/projectiles/FireballAnim.png", after_scale=anim_size) self.animation = Animation.from_surface_w(animation_surf, self.base_size[0], 5)
class GenericLevel(BaseLevel): source = filename _leveldata = load_level_data_from_file(filename, is_special=expected_special) raw_layout, start_entries, bg_name = _leveldata start_entries_rev = {v: k for k, v in start_entries.items()} bg_tile = imglib.load_image_from_file(bg_name) passages = [] for k, v in start_entries.items(): if v is not None: passages.append(k)
class EtherealSword(SimpleProjectile): hostile = False friendly = True base_size = (30, 12) base_image = imglib.load_image_from_file( "images/sl/projectiles/EtherealSword.png", after_scale=base_size) image_r = imglib.all_rotations(base_image) size_r = imglib.ValueRotationDependent( *[surface.get_size() for surface in image_r.as_list]) speed = 10 damage = 0.5
class Fireball(AbstractSpell): icon = imglib.load_image_from_file("images/pt/fireball-red-2.png", after_scale=base_icon_size) tree_pos = (0, -100) mana_cost = 20 @requires_mana(mana_cost) def cast(self): projectile = projectiles.Fireball( self.player.level, self.player.rect.center, norm_vector=self.player.best_heading_vector) self.player.level.sprites.append(projectile)
class PoisonRune(AbstractSpell): icon = imglib.load_image_from_file("images/pt/shielding-acid-3.png", after_scale=base_icon_size) tree_pos = (-100, 0) mana_cost = 25 count_min, count_max = 2, 4 dist = 20 class Rune(BaseSprite): friendly = True hostile = False cachable = False size = (20, 20) damage = 0.5 aoe = 50 damage_aoe = 0.25 surface = imglib.load_image_from_file( "images/sl/spells/RunePoison.png", after_scale=size) def __init__(self, level, pos): super().__init__() self.level = level self.rect = pygame.Rect((0, 0), self.size) self.rect.center = pos def update(self): if self.simple_deal_damage(): self.level.sprites.remove(self) for i in range(random.randint(4, 7)): p = particles.Particle.from_sprite(self, 4, utils.Vector.uniform(3), 50, Color.Green) self.level.particles.append(p) for sprite in self.level.hostile_sprites: if utils.dist(self.rect.center, sprite.rect.center) < self.aoe: sprite.take_damage(self.damage_aoe) @requires_mana(mana_cost) def cast(self): level = self.player.level pos = self.player.rect.center vec = self.player.best_heading_vector rune = self.Rune( level, (pos[0] + vec.x * self.dist, pos[1] + vec.y * self.dist)) level.sprites.append(rune)
class HiddenRoomTile(BaseTile): needs_update = False passable = True transparent = False flags_template = FlagSet(TileFlags.PartOfHiddenRoom) drawn_surface = imglib.load_image_from_file("images/dd/env/Wall.png", after_scale=tile_size_t) uncovered_drawn_surface = pygame.Surface(tile_size_t) uncovered_drawn_surface.fill(Color.Black) uncovered_drawn_surface.set_colorkey(Color.Black) def __init__(self, level, col_idx, row_idx): super().__init__(level, col_idx, row_idx) self.uncovered = False def uncover(self): self.uncovered = True self.drawn_surface = self.uncovered_drawn_surface self.transparent = True
class Ember(EasedProjectile): hostile = False friendly = True size = (4, 6) surface = imglib.load_image_from_file("images/sl/projectiles/Ember.png", after_scale=size) dist = 100 dist_diff = 20 length = 50 damage = 0.25 caused_effect = statuseffects.Burning effect_length = 60 effect_chance = 1 / 2 def deal_damage(self, sprite): super().deal_damage(sprite) if random.uniform(0, 1) <= self.effect_chance: tick = self.level.parent.game.ticks effect = self.caused_effect(sprite, tick, self.effect_length) sprite.status_effects.add(effect)
class SkeletonArcher(BaseEnemy): base_move_speed = 1 base_damage = 0.1 base_max_health_points = 1.5 damage_on_player_touch = True size = (30, 30) surface = imglib.load_image_from_file( "images/sl/enemies/SkeletonArcher.png", after_scale=size) shot_cooldown = 70 def __init__(self, level, spawner_tile): super().__init__(level, spawner_tile) self.next_shot = self.shot_cooldown self.last_rect = None self.path_obstructed = False self.moving = {k: False for k in base_directions} self.last_path_target = None self.path_to_player = [] self.current_target = None def update(self): super().update() player = self.level.parent.player for p in pathfinding.get_sprite_path_npoints(self, player): if not self.level.layout[p[1]][p[0]].passable: self.path_obstructed = True self.moving = {k: False for k in base_directions} break else: self.path_obstructed = False self.moving = False self.path_to_player = [] self.current_target = None if self.next_shot <= 0 and not self.path_obstructed: p = projectiles.Arrow.towards(self.level, self.rect.center, player.rect.center) self.level.sprites.append(p) self.next_shot = self.shot_cooldown * (self.base_move_speed / self.move_speed) self.next_shot -= 1 cpoint = self.closest_tile_index p1, p2 = cpoint, player.closest_tile_index if self.path_obstructed and self.last_path_target != p2: self.last_path_target = p2 self.path_to_player = pathfinding.a_star_in_level( p1, p2, self.level.layout) if self.path_to_player: while self.current_target is None or self.current_target == self.rect.center: p = self.path_to_player.pop() self.current_target = self.level.layout[p[1]][p[0]].rect.center t, c = self.current_target, self.rect.center d1, d2 = utils.sign(t[0] - c[0]), utils.sign(t[1] - c[1]) if d1 == -1: self.moving["left"] = True elif d1 == 1: self.moving["right"] = True if d2 == -1: self.moving["up"] = True elif d2 == 1: self.moving["down"] = True self.handle_moving()
class HeartsWidget(AbstractUIWidget): heart_size = config_ui["heart_size"] hearts_pos = config_ui["hearts_pos"] hearts_gap = config_ui["hearts_gap"] heart_img = imglib.load_image_from_file("images/sl/hearts/Full.png", after_scale=heart_size) halfheart_img = imglib.load_image_from_file("images/sl/hearts/Half.png", after_scale=heart_size) emptyheart_img = imglib.load_image_from_file("images/sl/hearts/Empty.png", after_scale=heart_size) # Invulnerable hearts (empty invulnerable heart is the same as the normal) heartinv_img = imglib.load_image_from_file("images/sl/hearts/FullInv.png", after_scale=heart_size) halfheartinv_img = imglib.load_image_from_file( "images/sl/hearts/HalfInv.png", after_scale=heart_size) def __init__(self, game, player): super().__init__(game, player) self.heart_draws = [] self.top_heart = None self.hearts_gap_default = self.hearts_gap self.update_hearts() def update(self): if self.player.last_health_points != self.player.health_points or \ bool(self.player.last_invincibility_ticks) != bool(self.player.invincibility_ticks) or \ math.ceil(self.player.max_health_points) != len(self.heart_draws): self.hearts_gap = self.hearts_gap_default self.update_hearts() def update_hearts(self): self.heart_draws.clear() self.top_heart = 0 invincible = bool(self.player.invincibility_ticks) heart_img = self.heartinv_img if invincible else self.heart_img halfheart_img = self.halfheartinv_img if invincible else self.halfheart_img emptyheart_img = self.emptyheart_img health = self.player.health_points hearts = math.ceil(self.player.max_health_points) rect = pygame.Rect(self.hearts_pos, self.heart_size) inside_fix = 10 gap = self.hearts_gap drawn = 0 # Distribute the hearts as needed if health > 0: for h in range(math.floor(health)): self.heart_draws.append((heart_img, rect.copy())) drawn += 1 rect.x += self.heart_size[0] + gap self.top_heart = drawn - 1 if health % 1: surface = emptyheart_img.copy() width = inside_fix / 2 + round( (rect.width - inside_fix) * (health % 1)) if width == rect.width - inside_fix / 2: width = rect.width area = pygame.Rect(0, 0, width, rect.height) surface.blit(heart_img, (0, 0), area) self.heart_draws.append((surface, rect.copy())) drawn += 1 rect.x += self.heart_size[0] + gap self.top_heart = drawn - 1 for h in range(hearts - drawn): self.heart_draws.append((emptyheart_img, rect.copy())) drawn += 1 rect.x += self.heart_size[0] + gap else: for h in range(math.ceil(self.player.max_health_points)): self.heart_draws.append((emptyheart_img, rect.copy())) drawn += 1 rect.x += self.heart_size[0] + gap if rect.x > 500 and self.hearts_gap > -self.heart_size[0]: self.hearts_gap -= 1 self.update_hearts() def draw(self, screen): for i, pair in enumerate(self.heart_draws): if i == self.top_heart: continue img, rect = pair screen.blit(img, rect) top = self.heart_draws[self.top_heart] screen.blit(top[0], top[1])
class IceBeam(ChanneledSpell): icon = imglib.load_image_from_file("images/pt/beam-blue-2.png", after_scale=base_icon_size) tree_pos = (100, 0) mana_channel_cost = 0.25 cooldown = 1 # If it works, it ain't broken. class BeamProjectile(projectiles.OmniProjectile): rotating = True hostile = False friendly = True base_size = (10, 5) base_image = imglib.load_image_from_file( "images/sl/projectiles/BeamBlue.png", after_scale=base_size) speed = base_size[0] - 1 damage = 0.003 particle_chance = 1 / 4 particle_spread = 30 particle_speed = 2 charged_particle_chance = 1 / 100 caused_effect = statuseffects.Chilled effect_length = 120 def __init__(self, level, pos, *, centerpos=True, norm_vector, charged=True, retain=lambda: True, reason=None): super().__init__(level, pos, centerpos=centerpos, norm_vector=norm_vector) self.charged = charged self.retain = retain self.reason = reason self.moving = True # Used to make the beam go deeper into the wall after collision self.destroy_on_next = False def update(self): if self.moving: self.xbuf += self.velx self.ybuf += self.vely self.rect.x, self.rect.y = self.xbuf, self.ybuf else: self.simple_deal_damage() if self.reason is None: if self.charged and random.uniform( 0, 1) <= self.charged_particle_chance: p = particles.Particle.from_sprite(self, 3, utils.Vector.uniform(1), 50, Color.lBlue) self.level.particles.append(p) elif random.uniform(0, 1) <= self.particle_chance: norm = self.norm_vector spread = self.particle_spread source_sprite = self if self.reason == projectiles.DestroyReason.Collision: vel = utils.Vector.random_spread( norm, spread).opposite() * self.particle_speed elif self.reason == projectiles.DestroyReason.DamageDeal: vel = utils.Vector.uniform(self.particle_speed) source_sprite = self.last_attacked_sprite else: vel = utils.Vector(0, 0) p = particles.Particle.from_sprite(source_sprite, 4, vel, 40, Color.lBlue) self.level.particles.append(p) def draw(self, screen, pos_fix=(0, 0)): super().draw(screen, pos_fix) if not self.retain(): self.destroy() def deal_damage(self, sprite): super().deal_damage(sprite) tick = self.level.parent.game.ticks eff = self.caused_effect(sprite, tick, self.effect_length) sprite.status_effects.add(eff) def __init__(self, player): super().__init__(player) self.next_beam = self.cooldown self.stationary_time = 0 self.last_tick_cast = -1 self.last_vec = None self.last_pos = None self.last_col = False self.beams = [] @requires_channel_mana(mana_channel_cost) def cast(self): level = self.player.level vec = self.player.best_heading_vector pos = self.player.rect.center if not any(self.player.moving.values() ) and self.player.game.ticks == self.last_tick_cast + 1: self.stationary_time += 1 else: self.stationary_time = 0 dest = False col = False any_beam = None ens = [] if self.beams: any_beam = self.beams[0] ens = any_beam.get_local_enemy_sprites() if self.beams and not any_beam in self.player.level.sprites: dest = True if not dest and self.last_vec != vec or self.last_pos != pos: dest = True if not dest and self.beams and ens: for b in self.beams: for e in ens: if b.rect.colliderect(e.rect): dest = True col = True break else: continue break if not dest and not col and self.last_col: dest = True if not dest and self.stationary_time >= 60 and not self.last_charged: dest = True if dest: self.destroy_beams() self.repopulate_beams() self.last_tick_cast = self.player.game.ticks self.last_vec = vec self.last_pos = pos self.last_col = col self.last_charged = self.stationary_time >= 60 def destroy_beams(self): for b in self.beams: if not b.destroyed: b.destroy() self.beams.clear() def repopulate_beams(self): level = self.player.level vec = self.player.best_heading_vector pos = self.player.rect.center i = 0 added = [] hit = [] stop_on_next = False first_collide = False while not stop_on_next: p = self.BeamProjectile(level, pos, norm_vector=vec, charged=self.stationary_time >= 60, retain=(lambda: self.cast_this_tick)) for v in range(i): p.xbuf += p.velx p.ybuf += p.vely p.rect.x, p.rect.y = p.xbuf, p.ybuf if not p.inside_level: break if p.get_collision_nearby(): p.reason = projectiles.DestroyReason.Collision stop_on_next = True for sprite in p.get_local_enemy_sprites(): if sprite in hit: continue if p.rect.colliderect(sprite.rect): p.last_attacked_sprite = sprite p.reason = projectiles.DestroyReason.DamageDeal hit.append(sprite) sprite.take_damage(p.damage) if not p.charged or len(hit) >= 3: stop_on_next = True break for sprite in self.beams: if p.rect.colliderect(sprite.rect): if i == 0: first_collide = True stop_on_next = True break if first_collide: break added.append(p) i += 1 for a in added: a.moving = False level.sprites.append(a) self.beams.append(a)
class WallTile(BaseTile): needs_update = False passable = False transparent = False drawn_surface = imglib.load_image_from_file("images/dd/env/Wall.png", after_scale=tile_size_t)
class FlyingBoomerang(OmniProjectile): hostile = False friendly = True base_size = (16, 16) base_image = imglib.load_image_from_file("images/sl/items/Boomerang.png", after_scale=base_size) damage = 0 # Override normal damage deal max_damage = 0.75 damage_loss_mul = 0.6 rotation_speed = 7 base_length = 40 back_speed = 10 curve_spread = 30 curve_point = 0.9 curve_back_spread = 60 curve_back_point = 0.2 def __init__(self, level, pos, *, centerpos=True, source_sprite, target): super().__init__(level, pos, centerpos=centerpos, norm_vector=utils.Vector(0, 0)) self.source_sprite = source_sprite # It always comes back! self.target = target self.dist = utils.dist(self.pos, self.target) self.surface = self.base_image self.length = self.base_length self.curve_points = None self.curve = self.new_curve() self.time_trans = lambda t: utils.translate_to_zero_to_one_bounds( t, (0, self.length)) self.time = 0 self.rotation = 0 self.coming_back = False self.coming_back_curve = False self.hit = set() self.act_damage = self.max_damage self.last_source_sprite_pos = self.source_sprite.rect.topleft self.last_adist = self.dist def update(self): self.rotation += self.rotation_speed self.rotation %= 360 self.surface = imglib.rotate(self.base_image, self.rotation) new_rect = self.surface.get_rect() new_rect.center = self.rect.center self.rect = new_rect for sprite in self.get_local_enemy_sprites(): if sprite not in self.hit and self.rect.colliderect(sprite.rect): self.hit.add(sprite) sprite.take_damage(self.act_damage) self.act_damage *= self.damage_loss_mul if self.simple_tick() == TickStatus.Destroy and \ (self.destroy_reason != DestroyReason.DamageDeal and self.act_damage > 0.2): if self.rect.colliderect(self.source_sprite.rect): super().destroy() return self.coming_back = True else: self.destroy_reason = None if (self.coming_back or self.coming_back_curve) and self.rect.colliderect( self.source_sprite.rect): super().destroy() return if self.coming_back: self.curve = None self.curve_points = None # Move in a straight line ignoring everything vec_back = utils.Vector.from_points(self.source_sprite.rect.center, self.rect.center) move = self.back_speed * vec_back.normalize() self.xbuf += move.x self.ybuf += move.y self.rect.centerx, self.rect.centery = self.xbuf, self.ybuf else: # Move in a curve, collision breaks this state change = self.source_sprite.rect.topleft != self.last_source_sprite_pos if self.coming_back_curve and change: self.curve = self.new_back_curve() if self.time > self.length: if not self.coming_back_curve: self.time = 0 self.curve = self.new_back_curve() self.coming_back_curve = True else: self.coming_back = True else: curve_pos = self.curve(self.time_trans(self.time)) self.xbuf, self.ybuf = self.rect.center = (curve_pos.x, curve_pos.y) self.time += 1 self.last_source_sprite_pos = self.source_sprite.rect.topleft def destroy(self): pass def new_curve(self): p = utils.break_segment(self.rect.center, self.target, self.curve_spread, self.curve_point) self.curve_points = p return utils.bezier(*p) def new_back_curve(self): # Make the boomerang go faster if the player is running away from it # THERE'S NO ESCAPE # IT WILL COME BACK adist = utils.dist(self.rect.center, self.source_sprite.rect.center) if adist < self.last_adist: d = self.dist / adist if d > 1: d = 1 / d d = d**(1 / 10) d = max(d, 0.2) self.length = self.base_length * d self.time = (self.time - 1) * (self.base_length / self.length) self.time = max(self.time, 1) self.last_adist = adist p = utils.break_segment(self.rect.center, self.source_sprite.rect.center, self.curve_back_spread, self.curve_back_point) self.curve_points = p return utils.bezier(*p)
class Fireball(OmniProjectile): hostile = False friendly = True rotating = False base_size = (16, 16) base_image = imglib.load_image_from_file( "images/sl/projectiles/Fireball.png", after_scale=base_size) animation_frames = 5 speed = 14 particle_spread = 35 particle_speed = 5 damage = 0.5 aoe = 40 damage_aoe = 0.25 caused_effect = statuseffects.Burning effect_length = 90 aoe_effect_chance = 1 / 2 def __init__(self, level, pos, *, centerpos=True, norm_vector): super().__init__(level, pos, centerpos=centerpos, norm_vector=norm_vector) anim_size = (self.base_size[0] * self.animation_frames, self.base_size[1]) animation_surf = imglib.load_image_from_file( "images/sl/projectiles/FireballAnim.png", after_scale=anim_size) self.animation = Animation.from_surface_w(animation_surf, self.base_size[0], 5) def update(self): super().update() self.animation.update() self.surface = self.animation.surface def destroy(self): super().destroy() norm, spread = self.norm_vector, self.particle_spread # Different particles depending on the destroy_reason # (collision - bouncing off the wall, damage deal - around the target sprite, # otherwise - assume the projectile is out-of-view anyways and don't spawn particles) for i in range(random.randint(5, 8)): if self.destroy_reason == DestroyReason.Collision: vel = utils.Vector.random_spread( norm, spread).opposite() * self.particle_speed source_sprite = self elif self.destroy_reason == DestroyReason.DamageDeal: vel = utils.Vector.uniform(self.particle_speed) source_sprite = self.last_attacked_sprite else: break color = random.choice((Color.Yellow, Color.Orange, Color.Red)) p = particles.Particle.from_sprite(source_sprite, 4, vel, 30, color) self.level.particles.append(p) # AOE Damage for sprite in self.get_local_enemy_sprites(): if utils.dist(self.rect.center, sprite.rect.center) < self.aoe: sprite.take_damage(self.damage_aoe) if random.uniform(0, 1) <= self.aoe_effect_chance: tick = self.level.parent.game.ticks effect = self.caused_effect(sprite, tick, self.effect_length) sprite.status_effects.add(effect) def deal_damage(self, sprite): super().deal_damage(sprite) tick = self.level.parent.game.ticks effect = self.caused_effect(sprite, tick, self.effect_length) sprite.status_effects.add(effect)
class HiddenRoomDoorTile(HiddenRoomTile): drawn_surface = imglib.load_image_from_file("images/dd/env/DoorOnWall.png", after_scale=tile_size_t)
class MinimapWidget(AbstractUIWidget): minimap_tile = config_ui["minimap_blocksize"] minimap_tile_t = (minimap_tile, minimap_tile) minimap_tiles = config_ui["minimap_tiles"] question_mark = imglib.load_image_from_file( "images/sl/minimap/QuestionMarkM.png", after_scale=minimap_tile_t) tile_empty = imglib.load_image_from_file("images/dd/env/Bricks.png", after_scale=minimap_tile_t) tile_wall = imglib.load_image_from_file("images/dd/env/WallSmall.png", after_scale=minimap_tile_t) border_color = (0, 29, 109) def __init__(self, game, player): super().__init__(game, player) full_minimap_size_px = (self.game.vars["mapsize"][0] * self.minimap_tile, self.game.vars["mapsize"][1] * self.minimap_tile) self.background = imglib.repeated_image_texture( self.question_mark, full_minimap_size_px) self.full_surface = pygame.Surface(full_minimap_size_px) self.surface = pygame.Surface(config_ui["minimap_size"]) self.rect = self.surface.get_rect() self.border = imglib.color_border(config_ui["minimap_size"], self.border_color, 4, nowarn=True) self.tile_current = self.new_tile_current_surface() def update_full(self): # Redraw the entire minimap surface self.full_surface.fill(Color.Black) self.full_surface.blit(self.background, (0, 0)) mazepos = self.game.vars["player_mazepos"] for row, irow in enumerate(self.game.vars["maze"]): for col, bit in enumerate(irow): x, y = col * self.minimap_tile, row * self.minimap_tile rect = pygame.Rect((x, y), self.minimap_tile_t) if (col, row) == mazepos: tile = self.tile_current elif self.player.map_reveal[row][col]: tile = self.tile_wall if bit else self.tile_empty else: continue self.full_surface.blit(tile, rect) def update_part(self): # Redraw the needed part of the entire minimap surface to the display UI element mazepos = self.game.vars["player_mazepos"] mtopleftidx = tuple( (self.minimap_tiles[i] - 1) / 2 - mazepos[i] for i in range(2)) mtopleft = mtopleftidx[0] * self.minimap_tile, mtopleftidx[ 1] * self.minimap_tile self.rect.topleft = mtopleft self.surface.fill(Color.Black) self.surface.blit(self.full_surface, self.rect) self.surface.blit(self.border, (0, 0)) def update_on_new_level(self): self.update_full() self.update_part() def update(self): pass def draw(self, screen): screen.blit(self.surface, config_ui["minimap_pos"]) def new_tile_current_surface(self): surface = self.tile_empty.copy() mini_player_size = (int(self.minimap_tile / 1.2), int(self.minimap_tile / 1.2)) scaled_player = imglib.scale(self.player.surface, mini_player_size) s_rect = scaled_player.get_rect() mini_player_pos = (int((self.minimap_tile - s_rect.width) / 2), int((self.minimap_tile - s_rect.height) / 2)) surface.blit(scaled_player, mini_player_pos) return surface
class Chest(BaseContainer): drawn_surface = imglib.load_image_from_file("images/sl/env/Chest.png", after_scale=tile_size_t)
class StarsWidget(AbstractUIWidget): mana_per_star = config_ui["mana_per_star"] star_main_color = config_ui["star_main_color"] star_size = config_ui["star_size"] stars_pos = config_ui["stars_pos"] stars_gap = config_ui["stars_gap"] star_img = imglib.load_image_from_file("images/sl/stars/Star.png", after_scale=star_size) main_r, main_g, main_b = star_main_color star_images = [] lowest_alpha = 200 _get_trans_color = lambda v, fr, m=255: v + (m - v) * fr for i in range(mana_per_star + 1): star_array = pygame.PixelArray(star_img.copy()) _fr = 1 - (i / mana_per_star) if _fr > 0: repcolor = (_get_trans_color(main_r, _fr, lowest_alpha), _get_trans_color(main_g, _fr, lowest_alpha), _get_trans_color(main_b, _fr)) star_array.replace(star_main_color, repcolor, 0.4) star_images.append(star_array.surface) del star_array def __init__(self, game, player): super().__init__(game, player) self.star_draws = [] self.top_star = None self.stars_gap_default = self.stars_gap self.update_stars() def update(self): if self.player.last_mana_points != self.player.mana_points or \ math.ceil(self.player.max_mana_points / self.mana_per_star) != len(self.star_draws): self.stars_gap = self.stars_gap_default self.update_stars() def update_stars(self): self.star_draws.clear() self.top_star = 0 rect = pygame.Rect(self.stars_pos, self.star_size) count = math.ceil(self.player.max_mana_points / self.mana_per_star) drawn = 0 for i in range(self.player.mana_points // self.mana_per_star): self.star_draws.append( (self.star_images[self.mana_per_star], rect.copy())) rect.x += self.star_size[0] + self.stars_gap drawn += 1 self.top_star = drawn - 1 if drawn != count: self.star_draws.append( (self.star_images[self.player.mana_points % 20], rect.copy())) rect.x += self.star_size[0] + self.stars_gap drawn += 1 self.top_star = drawn - 1 while drawn != count: self.star_draws.append((self.star_images[0], rect.copy())) rect.x += self.star_size[0] + self.stars_gap drawn += 1 if rect.x > 500 and self.stars_gap > -self.star_size[0]: self.stars_gap -= 1 self.update_stars() def draw(self, screen): for i, pair in enumerate(self.star_draws): if i == self.top_star: continue img, rect = pair screen.blit(img, rect) top = self.star_draws[self.top_star] screen.blit(top[0], top[1])
class PlayerCharacter(BaseSprite): is_entity = True size = (32, 32) surface = imglib.load_image_from_file("images/dd/player/HeroBase.png", after_scale=size) attributes = [ "move_speed", "max_health_points", "max_mana_points", "vision_radius" ] base_max_health_points = 3 base_max_mana_points = 60 base_move_speed = 4 base_vision_radius = 16 starting_items = [ playeritems.Sword, playeritems.EnchantedSword, playeritems.FireballStaff, playeritems.Boomerang ] starting_spells = [spells.Embers] invincibility_ticks_on_damage = 120 sprint_move_speed_gain = 4 mana_regen_delay = 50 mana_regen_args = (0, 0.7, 125) mana_regen_moving_malus_mul = 0.2 def __init__(self, game): super().__init__() self.game = game # Set by the state self.parent_state = None self.level = None self.rect = pygame.Rect((0, 0), self.size) self.precache = {} self.inventory = playerinventory.PlayerInventory(self) for item_cls in self.starting_items: self.inventory.add_item(item_cls(self)) self.selected_item_idx = 0 self.unlocked_spells = self.starting_spells.copy() self.selected_spell = self.unlocked_spells[0](self) self.selected_spell.on_select() self.last_selected_spell = self.selected_spell self.reset_attributes() self.activate_tile = False self.moving = { "left": False, "right": False, "up": False, "down": False } self.move_sprint = False self.rotation = "right" self.going_through_door = False self.crouching = False self.use_item = False self.cast_spell = False self.fov_enabled = self.game.vars["enable_fov"] if self.fov_enabled: self.computed_fov_map = None self.level_vision = None self.health_points = self.max_health_points self.last_health_points = self.health_points self.invincibility_ticks = 0 self.last_invincibility_ticks = self.invincibility_ticks self.mana_points = self.max_mana_points self.last_mana_points = self.mana_points self.mana_ticks_until_regen = -1 self.mana_regen_tick = 0 self.mana_regen_buffer = 0 self.status_effects = statuseffects.StatusEffects( self, lambda: self.game.ticks) self.map_reveal = self.new_gamemap_map() self.widgets = [] # Populated by a widget self.minimap = playerui.MinimapWidget(self.game, self) self.hearts = playerui.HeartsWidget(self.game, self) self.stars = playerui.StarsWidget(self.game, self) self.item_box = playerui.SelectedItemBoxWidget(self.game, self) self.spell_box = playerui.SelectedSpellBoxWidget(self.game, self) def __repr__(self): return "<{} @ {}>".format(type(self).__name__, self.rect.topleft) def handle_events(self, events, pressed_keys, mouse_pos): self.activate_tile = False self.moving["left"] = pressed_keys[controls.Keys.Left] self.moving["right"] = pressed_keys[controls.Keys.Right] self.moving["up"] = pressed_keys[controls.Keys.Up] self.moving["down"] = pressed_keys[controls.Keys.Down] self.move_sprint = pressed_keys[controls.Keys.Sprint] self.crouching = pressed_keys[controls.Keys.Crouch] item = self.selected_item spell = self.selected_spell for event in events: if event.type == pygame.KEYDOWN: if event.key == controls.Keys.UseItem: if item is not None and not item.special_use: self.use_item = True elif event.key == controls.Keys.CastSpell: if spell is not None and not spell.special_cast: self.cast_spell = True elif event.key == controls.Keys.ActivateTile: self.activate_tile = True elif event.key == controls.Keys.Left: self.rotation = "left" elif event.key == controls.Keys.Right: self.rotation = "right" elif event.key == controls.Keys.Up: self.rotation = "up" elif event.key == controls.Keys.Down: self.rotation = "down" if self.game.use_mouse and event.type == pygame.MOUSEBUTTONDOWN: if event.button == 1: if item is not None and not item.special_use: self.use_item = True elif event.button == 3 and not spell.special_cast: self.cast_spell = True if item is not None and item.special_use: self.use_item = item.can_use(events, pressed_keys, mouse_pos, controls) if spell is not None and spell.special_cast: self.cast_spell = spell.can_cast(events, pressed_keys, mouse_pos, controls) def update(self): top = self.game.top_state self.reset_attributes() self.status_effects.update() if self.use_item: self.selected_item.use() self.use_item = False if self.selected_item is not None: self.selected_item.update() if self.selected_spell is not self.last_selected_spell: self.last_selected_spell.on_deselect() self.selected_spell.on_select() if self.cast_spell: self.selected_spell.cast_this_tick = True self.selected_spell.cast() self.cast_spell = False else: self.selected_spell.cast_this_tick = False if self.move_sprint: self.move_speed += self.sprint_move_speed_gain if self.last_mana_points <= self.mana_points < self.max_mana_points: if self.mana_ticks_until_regen != 0: if self.mana_ticks_until_regen == -1: self.mana_ticks_until_regen = self.mana_regen_delay if self.mana_ticks_until_regen >= 0: self.mana_ticks_until_regen -= 1 else: self.mana_ticks_until_regen = -1 self.mana_regen_buffer = 0 self.mana_regen_tick = 0 if self.mana_ticks_until_regen == 0: self.mana_regen_tick += 1 current_mana_regen = 0 if self.mana_regen_tick <= self.mana_regen_args[2]: current_mana_regen += easing.ease_circular_in( self.mana_regen_tick, *self.mana_regen_args) else: current_mana_regen += self.mana_regen_args[ 0] + self.mana_regen_args[1] if any(self.moving.values()): current_mana_regen *= self.mana_regen_moving_malus_mul self.mana_regen_buffer += current_mana_regen self.mana_points += math.floor(self.mana_regen_buffer) self.mana_regen_buffer %= 1 if self.mana_ticks_until_regen >= 0 and self.mana_points == self.max_mana_points: self.mana_ticks_until_regen = -1 self.mana_regen_buffer = 0 self.mana_regen_tick = 0 self.check_attribute_bounds() for widget in self.widgets: widget.update() self.moving_last = self.moving.copy() if not self.crouching: self.handle_moving() self.near_passage = None pcol, prow = self.closest_tile_index ptile = self.level.layout[prow][pcol] next_to = self.get_tiles_next_to() + [(pcol, prow)] # Near passages to other levels self.going_through_door = False for col, row in next_to: tile = self.level.layout[row][col] if tile.flags.Passage: self.near_passage = tile break else: if ptile.flags.Passage: self.near_passage = ptile if self.near_passage is not None: if self.activate_tile or \ (self.rect.left == 0 and self.moving_last["left"]) or \ (self.rect.right == screen_size[0] and self.moving_last["right"]) or \ (self.rect.top == 0 and self.moving_last["up"]) or \ (self.rect.bottom == screen_size[1] and self.moving_last["down"]): self.going_through_door = True # Near containers (chests) self.near_container = None for col, row in next_to: tile = self.level.layout[row][col] if tile.flags.Container: self.near_container = tile break if self.near_container is not None and self.activate_tile: surf = top.get_as_parent_surface() s = states.PlayerInventoryState(self.game, parent_surface=surf, container=self.near_container) self.parent_state.queue_state = s # FOV inside_level = 0 <= pcol < self.level.width and 0 <= prow < self.level.height if self.fov_enabled and inside_level and not self.computed_fov_map[ prow][pcol]: fovlib.calculate_fov(self.level.transparency_map, pcol, prow, self.vision_radius, dest=self.level_vision) self.computed_fov_map[prow][pcol] = True # Hidden rooms if ptile.flags.PartOfHiddenRoom and not ptile.uncovered: self.explore_room() # Invincibility ticks self.last_invincibility_ticks = self.invincibility_ticks if self.invincibility_ticks: self.invincibility_ticks -= 1 # Set last self.last_health_points = self.health_points self.last_mana_points = self.mana_points self.last_selected_spell = self.selected_spell def draw(self, screen, pos_fix=(0, 0), *, dui=True): if self.fov_enabled: # Draw black squares in places the player didn't see yet for row in self.level.layout: for tile in row: if not self.level_vision[tile.row_idx][tile.col_idx]: screen.fill(Color.Black, tile.rect.move(pos_fix)) super().draw(screen, pos_fix) if self.selected_item is not None: self.selected_item.draw(screen, pos_fix) def draw_ui(self, screen, pos_fix=(0, 0)): screen.fill( Color.Black, pygame.Rect(config_dungeon["topbar_position"], config_dungeon["topbar_size"])) for widget in self.widgets: widget.draw(screen) # ===== Methods ===== def create_cache(self): cache = deepcopy(self.precache) if "base_attributes" not in cache: cache["base_attributes"] = {} cache["base_attributes"]["move_speed"] = self.base_move_speed cache["base_attributes"][ "max_health_points"] = self.base_max_health_points cache["base_attributes"]["max_mana_points"] = self.base_max_mana_points cache["base_attributes"]["vision_radius"] = self.base_vision_radius if "status" not in cache: cache["status"] = {} cache["status"]["pos"] = self.rect.topleft cache["status"]["health_points"] = self.health_points cache["status"]["mana_points"] = self.mana_points cache["status"]["selected_item_idx"] = self.selected_item_idx cache["status"]["selected_spell"] = type(self.selected_spell) cache["map_reveal"] = self.map_reveal if "status_effects" not in cache: cache["status_effects"] = [] cache["status_effects"].extend(self.status_effects.create_cache()) if "inventory" not in cache: cache["inventory"] = {} for i, item in enumerate(self.inventory.slots): if item is not None: icache = item.create_cache() # e.g. consumable items won't be added to caches, # since they already applied the status effect # and the rest of the effect is cosmetical if icache is not None: cache["inventory"][i] = icache if "unlocked_spells" not in cache: cache["unlocked_spells"] = [] for spelltype in self.unlocked_spells: cache["unlocked_spells"].append(spelltype) return cache def load_cache(self, cache): self.base_move_speed = cache["base_attributes"]["move_speed"] self.base_max_health_points = cache["base_attributes"][ "max_health_points"] self.base_max_mana_points = cache["base_attributes"]["max_mana_points"] self.base_vision_radius = cache["base_attributes"]["vision_radius"] self.rect.topleft = cache["status"]["pos"] self.health_points = cache["status"]["health_points"] self.mana_points = cache["status"]["mana_points"] self.selected_item_idx = cache["status"]["selected_item_idx"] self.selected_spell = cache["status"]["selected_spell"](self) self.map_reveal = cache["map_reveal"] self.status_effects.load_cache(cache["status_effects"]) self.inventory.clear_items() for i, item_cache in cache["inventory"].items(): self.inventory.slots[int(i)] = item_cache["type"].from_cache( self, item_cache) self.unlocked_spells.clear() for spelltype in cache["unlocked_spells"]: self.unlocked_spells.append(spelltype) for widget in self.widgets: widget.update() def new_empty_level_map(self): return [[False for _ in range(self.level.width)] for _ in range(self.level.height)] def new_gamemap_map(self): return [[False for _ in range(self.game.vars["mapsize"][0])] for _ in range(self.game.vars["mapsize"][1])] def reset_attributes(self): self.move_speed = self.base_move_speed self.max_health_points = self.base_max_health_points self.max_mana_points = self.base_max_mana_points self.vision_radius = self.base_vision_radius def new_damage_particle(self, damage=1): maxvel = utils.Vector.uniform(2 * min(damage, 2.5)) return particles.Particle.from_sprite(self, 5, maxvel, 200, Color.Red) def check_attribute_bounds(self): if self.health_points < 0: self.health_points = 0 if self.health_points > self.max_health_points: self.health_points = self.max_health_points if self.mana_points < 0: self.mana_points = 0 if self.mana_points > self.max_mana_points: self.mana_points = self.max_mana_points # Health def take_damage(self, value, *, ignore_invincibility=False, doparticles=True): if not self.invincibility_ticks or ignore_invincibility: if value > 0: self.invincibility_ticks = self.invincibility_ticks_on_damage self.health_points -= value self.health_points = _clamp(0, self.max_health_points, self.health_points) if value != 0: self.on_damage(value) def heal(self, value): return self.take_damage(-value, ignore_invincibility=True) # Updates def on_new_level(self): if self.fov_enabled: self.computed_fov_map = self.new_empty_level_map() self.level_vision = self.new_empty_level_map() self.reveal_nearby_map_tiles() self.minimap.update_on_new_level() def on_damage(self, value, *, doparticles=True): if doparticles and value > 0: mn = max(1, int(3 * value)) mx = max(5, int(15 * value)) for i in range(random.randint(mn, mx)): self.level.particles.append(self.new_damage_particle(value)) # Minimap def reveal_nearby_map_tiles(self): col, row = self.game.vars["player_mazepos"] width, height = self.game.vars["mapsize"] self.map_reveal[row][col] = True for ncol, nrow in [(col + 1, row), (col - 1, row), (col, row + 1), (col, row - 1), (col + 1, row + 1), (col - 1, row + 1), (col + 1, row - 1), (col - 1, row - 1)]: if 0 <= ncol < width and 0 <= nrow < height: self.map_reveal[nrow][ncol] = True # Level def explore_room(self): scol, srow = self.closest_tile_index width, height = self.level.width, self.level.height queue = deque() queue.appendleft((scol, srow)) visited = {(scol, srow)} while queue: col, row = queue.pop() tile = self.level.layout[row][col] if tile.flags.PartOfHiddenRoom: tile.uncover() checked = [(col - 1, row), (col + 1, row), (col, row - 1), (col, row + 1)] for ncol, nrow in checked: tup = (ncol, nrow) if 0 <= ncol < width - 1 and 0 <= nrow < height - 1 and tup not in visited: visited.add(tup) queue.appendleft(tup) self.level.force_render_update = True # Status @property def dying(self): return self.health_points < self.max_health_points # Other @property def selected_item(self): return self.inventory.slots[self.selected_item_idx] @property def rotation_vector(self): if self.rotation == "left": return utils.Vector(-1, 0) elif self.rotation == "right": return utils.Vector(1, 0) elif self.rotation == "up": return utils.Vector(0, -1) elif self.rotation == "down": return utils.Vector(0, 1) @property def to_mouse_vector(self): fix = [-n for n in config_dungeon["level_surface_position"]] return utils.norm_vector_to_mouse(self.rect.center, fix) @property def best_heading_vector(self): if self.game.use_mouse: return self.to_mouse_vector else: return self.rotation_vector # Cheats def reveal_all_map_tiles(self): for row in self.map_reveal: for i in range(len(row)): row[i] = True self.minimap.update_full() def unlock_all_spells(self): for name, spell in spells.register.items(): self.unlocked_spells.append(spell)
class BeamProjectile(projectiles.OmniProjectile): rotating = True hostile = False friendly = True base_size = (10, 5) base_image = imglib.load_image_from_file( "images/sl/projectiles/BeamBlue.png", after_scale=base_size) speed = base_size[0] - 1 damage = 0.003 particle_chance = 1 / 4 particle_spread = 30 particle_speed = 2 charged_particle_chance = 1 / 100 caused_effect = statuseffects.Chilled effect_length = 120 def __init__(self, level, pos, *, centerpos=True, norm_vector, charged=True, retain=lambda: True, reason=None): super().__init__(level, pos, centerpos=centerpos, norm_vector=norm_vector) self.charged = charged self.retain = retain self.reason = reason self.moving = True # Used to make the beam go deeper into the wall after collision self.destroy_on_next = False def update(self): if self.moving: self.xbuf += self.velx self.ybuf += self.vely self.rect.x, self.rect.y = self.xbuf, self.ybuf else: self.simple_deal_damage() if self.reason is None: if self.charged and random.uniform( 0, 1) <= self.charged_particle_chance: p = particles.Particle.from_sprite(self, 3, utils.Vector.uniform(1), 50, Color.lBlue) self.level.particles.append(p) elif random.uniform(0, 1) <= self.particle_chance: norm = self.norm_vector spread = self.particle_spread source_sprite = self if self.reason == projectiles.DestroyReason.Collision: vel = utils.Vector.random_spread( norm, spread).opposite() * self.particle_speed elif self.reason == projectiles.DestroyReason.DamageDeal: vel = utils.Vector.uniform(self.particle_speed) source_sprite = self.last_attacked_sprite else: vel = utils.Vector(0, 0) p = particles.Particle.from_sprite(source_sprite, 4, vel, 40, Color.lBlue) self.level.particles.append(p) def draw(self, screen, pos_fix=(0, 0)): super().draw(screen, pos_fix) if not self.retain(): self.destroy() def deal_damage(self, sprite): super().deal_damage(sprite) tick = self.level.parent.game.ticks eff = self.caused_effect(sprite, tick, self.effect_length) sprite.status_effects.add(eff)