class ConsumableInventory: def __init__(self, slots: Dict[int, List[ConsumableType]]): self.consumables_in_slots: Dict[int, List[ConsumableType]] = slots self.capacity_per_slot = 2 self.was_updated = Observable() def set_slots(self, slots: Dict[int, List[ConsumableType]]): self.consumables_in_slots = slots self.notify_observers() def has_space_for_more(self) -> bool: slots_with_space = [consumables for consumables in self.consumables_in_slots.values() if len(consumables) < self.capacity_per_slot] return len(slots_with_space) > 0 def add_consumable(self, consumable_type: ConsumableType): non_full_slots_with_same_type = [consumables for consumables in self.consumables_in_slots.values() if 0 < len(consumables) < self.capacity_per_slot and consumables[0] == consumable_type] if non_full_slots_with_same_type: non_full_slots_with_same_type[0].append(consumable_type) self.notify_observers() return empty_slots = [consumables for consumables in self.consumables_in_slots.values() if len(consumables) == 0] if empty_slots: empty_slots[0].append(consumable_type) self.notify_observers() return for consumables in self.consumables_in_slots.values(): if len(consumables) < self.capacity_per_slot: consumables.append(consumable_type) self.notify_observers() return raise Exception("No space for consumable!") def drag_consumable_between_inventory_slots(self, from_slot: int, to_slot: int): consumable_type_1 = self.remove_consumable_from_slot(from_slot) if len(self.consumables_in_slots[to_slot]) < self.capacity_per_slot: self.consumables_in_slots[to_slot].insert(0, consumable_type_1) else: consumable_type_2 = self.remove_consumable_from_slot(to_slot) self.consumables_in_slots[to_slot].insert(0, consumable_type_1) self.consumables_in_slots[from_slot].insert(0, consumable_type_2) self.notify_observers() def remove_consumable_from_slot(self, slot_number: int) -> Optional[ConsumableType]: if self.consumables_in_slots[slot_number]: removed = self.consumables_in_slots[slot_number].pop(0) self.notify_observers() return removed return None def get_consumable_at_slot(self, slot_number: int) -> Optional[ConsumableType]: return self.consumables_in_slots[slot_number][0] if len(self.consumables_in_slots[slot_number]) > 0 else None def notify_observers(self): self.was_updated.notify(self.consumables_in_slots)
def register_game_state_observers(game_state: GameState, ui_view: GameUiView, include_player_state: bool): game_state.game_world.player_movement_speed_was_updated.register_observer(ui_view.on_player_movement_speed_updated) game_state.game_world.notify_movement_speed_observers() # Must notify the initial state game_state.game_world.player_entity.movement_changed = Observable() game_state.game_world.player_entity.movement_changed.register_observer(play_or_stop_footstep_sounds) game_state.game_world.player_entity.position_changed = Observable() game_state.game_world.player_entity.position_changed.register_observer(ui_view.on_player_position_updated) game_state.game_world.player_entity.position_changed.register_observer( lambda _: ui_view.on_walls_seen([w.get_position() for w in game_state.get_walls_in_sight_of_player()])) game_state.game_world.player_entity.notify_position_observers() # Must notify the initial state if include_player_state: _register_player_state_observers(game_state.player_state, ui_view)
def __init__(self, slots: List[ItemInventorySlot]): self.slots = slots self.was_updated = Observable()
class ItemInventory: def __init__(self, slots: List[ItemInventorySlot]): self.slots = slots self.was_updated = Observable() def switch_item_slots(self, slot_1_index: int, slot_2_index: int) -> List[ItemActivationEvent]: slot_1 = self.slots[slot_1_index] slot_2 = self.slots[slot_2_index] content_1 = slot_1.item content_2 = slot_2.item events = [] is_switch_allowed = slot_2.can_contain_item( content_1) and slot_1.can_contain_item(content_2) if is_switch_allowed: if content_1: item_type_1 = slot_1.get_item_type() if slot_1.is_active() and not slot_2.is_active(): event_1 = ItemWasDeactivated(item_type_1) elif not slot_1.is_active() and slot_2.is_active(): event_1 = ItemWasActivated(item_type_1) else: event_1 = ItemActivationStateDidNotChange(item_type_1) events.append(event_1) if content_2: item_type_2 = slot_2.get_item_type() if slot_2.is_active() and not slot_1.is_active(): event_2 = ItemWasDeactivated(item_type_2) elif not slot_2.is_active() and slot_1.is_active(): event_2 = ItemWasActivated(item_type_2) else: event_2 = ItemActivationStateDidNotChange(item_type_2) events.append(event_2) slot_1.item = content_2 slot_2.item = content_1 self.notify_observers() return events def try_switch_item_at_slot(self, slot_index: int) -> List[ItemActivationEvent]: if self.slots[slot_index].is_empty(): return [] for other_slot_index in [ s for s in range(len(self.slots)) if s != slot_index ]: events = self.switch_item_slots(slot_index, other_slot_index) if events: self.notify_observers() return events return [] def has_item_in_inventory(self, item_type: ItemType): matches = [ slot for slot in self.slots if not slot.is_empty() and slot.get_item_type() == item_type ] if len(matches) > 0: return True def lose_item_from_inventory(self, item_type: ItemType): for slot_number in range(len(self.slots)): slot = self.slots[slot_number] if not slot.is_empty() and slot.get_item_type() == item_type: self.slots[slot_number].item = None self.notify_observers() return print("WARN: item not found in inventory: " + item_type.name) # Note: this will need to return events, if it's used for items that have effects def is_slot_empty(self, slot_index: int) -> bool: return self.slots[slot_index].is_empty() def try_add_item(self, item_effect, item_equipment_category: ItemEquipmentCategory) \ -> Optional[ItemActivationEvent]: item_in_slot = ItemInSlot(item_effect, item_equipment_category) empty_slot_index = self._find_empty_slot_for_item(item_in_slot) if empty_slot_index is not None: slot = self.slots[empty_slot_index] slot.item = item_in_slot self.notify_observers() if slot.is_active(): return ItemWasActivated(item_effect.get_item_type()) else: return ItemActivationStateDidNotChange( item_effect.get_item_type()) return None def put_item_in_inventory_slot( self, item_effect, item_equipment_category: ItemEquipmentCategory, slot_number: int) -> ItemActivationEvent: item_in_slot = ItemInSlot(item_effect, item_equipment_category) slot = self.slots[slot_number] if not slot.is_empty(): raise Exception("Can't put item in non-empty slot!") slot.item = item_in_slot self.notify_observers() if slot.is_active(): return ItemWasActivated(item_effect.get_item_type()) else: return ItemActivationStateDidNotChange(item_effect.get_item_type()) def _find_empty_slot_for_item(self, item: ItemInSlot) -> Optional[int]: empty_slot_indices = [ i for i in range(len(self.slots)) if self.slots[i].is_empty() and self.slots[i].can_contain_item(item) ] if empty_slot_indices: return empty_slot_indices[0] return None def get_item_type_in_slot(self, slot_index: int) -> ItemType: if self.slots[slot_index].is_empty(): raise Exception("Can't get item type from empty inventory slot: " + str(slot_index)) return self.slots[slot_index].get_item_type() def remove_item_from_slot(self, slot_index: int) -> ItemActivationEvent: slot = self.slots[slot_index] if slot.is_empty(): raise Exception("Can't remove item from empty inventory slot: " + str(slot_index)) item_type = slot.get_item_type() slot.item = None self.notify_observers() if slot.is_active(): return ItemWasDeactivated(item_type) return ItemActivationStateDidNotChange(item_type) def get_all_active_item_effects(self) -> List[Any]: return [ slot.item.item_effect for slot in self.slots if slot.is_active() and not slot.is_empty() ] def notify_observers(self): self.was_updated.notify(self.slots)
def run_one_frame(self, _time_passed: Millis) -> Optional[SceneTransition]: saved_player_state = self.flags.saved_player_state hero_start_level = self.flags.hero_start_level start_money = self.flags.start_money picked_hero = self.flags.picked_hero map_file_path = self.flags.map_file_path character_file = self.flags.character_file if saved_player_state: hero_from_saved_state = HeroId[saved_player_state.hero_id] if picked_hero is not None and picked_hero != hero_from_saved_state: raise Exception("Mismatch! Hero from saved state: " + str(hero_from_saved_state) + ", but picked hero: " + str(picked_hero)) picked_hero = hero_from_saved_state total_time_played_on_character = saved_player_state.total_time_played_on_character if saved_player_state else 0 # NPC's share a "global path finder" that needs to be initialized before we start creating NPCs. # TODO This is very messy path_finder = GlobalPathFinder() set_global_path_finder(path_finder) map_data = load_map_from_json_file(self.camera_size, map_file_path, picked_hero) path_finder.set_grid(map_data.game_state.pathfinder_wall_grid) game_state = map_data.game_state game_engine = GameEngine(game_state, self.ui_view.info_message) game_state.player_state.exp_was_updated.register_observer( self.ui_view.on_player_exp_updated) game_state.player_state.talents_were_updated.register_observer( self.ui_view.on_talents_updated) game_state.player_state.notify_talent_observers( ) # Must notify the initial state game_state.player_movement_speed_was_updated.register_observer( self.ui_view.on_player_movement_speed_updated) game_state.notify_movement_speed_observers( ) # Must notify the initial state game_state.player_state.stats_were_updated.register_observer( self.ui_view.on_player_stats_updated) game_state.player_state.notify_stats_observers( ) # Must notify the initial state game_engine.talent_was_unlocked.register_observer( self.ui_view.on_talent_was_unlocked) game_engine.ability_was_clicked.register_observer( self.ui_view.on_ability_was_clicked) game_engine.consumable_was_clicked.register_observer( self.ui_view.on_consumable_was_clicked) game_state.player_state.money_was_updated.register_observer( self.ui_view.on_money_updated) game_state.player_state.notify_money_observers( ) # Must notify the initial state game_state.player_state.abilities_were_updated.register_observer( self.ui_view.on_abilities_updated) game_state.player_state.notify_ability_observers( ) # Must notify the initial state game_state.player_state.cooldowns_were_updated.register_observer( self.ui_view.on_cooldowns_updated) game_state.player_state.health_resource.value_was_updated.register_observer( self.ui_view.on_health_updated) game_state.player_state.mana_resource.value_was_updated.register_observer( self.ui_view.on_mana_updated) game_state.player_state.buffs_were_updated.register_observer( self.ui_view.on_buffs_updated) game_state.player_state.quests_were_updated.register_observer( self.ui_view.on_player_quests_updated) game_state.player_entity.movement_changed = Observable() game_state.player_entity.movement_changed.register_observer( play_or_stop_footstep_sounds) game_state.player_entity.position_changed = Observable() game_state.player_entity.position_changed.register_observer( self.ui_view.on_player_position_updated) game_state.player_entity.position_changed.register_observer( lambda _: self.ui_view.on_walls_seen([ w.get_position() for w in game_state.get_walls_in_sight_of_player() ])) self.ui_view.on_world_area_updated(game_state.entire_world_area) # Must center camera before notifying player position as it affects which walls are shown on the minimap game_state.center_camera_on_player() game_state.player_entity.notify_position_observers( ) # Must notify the initial state if map_file_path == 'resources/maps/challenge.json': world_behavior = ChallengeBehavior(self.picking_hero_scene, self.challenge_complete_scene, game_state, self.ui_view.info_message, game_engine, self.flags) else: world_behavior = StoryBehavior(self.victory_screen_scene, game_state, self.ui_view.info_message) if saved_player_state: game_engine.gain_levels(saved_player_state.level - 1) game_state.player_state.gain_exp(saved_player_state.exp) game_engine.set_item_inventory([ ItemType[item] if item else None for item in saved_player_state.items ]) game_state.player_state.consumable_inventory = ConsumableInventory( { int(slot_number): [ConsumableType[c] for c in consumables] for (slot_number, consumables ) in saved_player_state.consumables_in_slots.items() }) game_state.player_state.modify_money(saved_player_state.money) for portal in game_state.portals: if portal.portal_id.name in saved_player_state.enabled_portals: sprite = saved_player_state.enabled_portals[ portal.portal_id.name] portal.activate(Sprite[sprite]) for tier_index, option_index in enumerate( saved_player_state.talent_tier_choices): if option_index is not None: pick_talent(game_state, tier_index, option_index) for completed_quest in saved_player_state.completed_quests: quest = get_quest(QuestId[completed_quest]) game_state.player_state.start_quest(quest) game_state.player_state.complete_quest(quest) for active_quest in saved_player_state.active_quests: quest = get_quest(QuestId[active_quest]) game_state.player_state.start_quest(quest) else: if hero_start_level > 1: game_engine.gain_levels(hero_start_level - 1) if start_money > 0: game_state.player_state.modify_money(start_money) game_state.player_state.item_inventory.was_updated.register_observer( self.ui_view.on_inventory_updated) game_state.player_state.item_inventory.notify_observers( ) # Must notify the initial state game_state.player_state.consumable_inventory.was_updated.register_observer( self.ui_view.on_consumables_updated) game_state.player_state.consumable_inventory.notify_observers( ) # Must notify the initial state self.ui_view.update_hero(game_state.player_state.hero_id) # When loading from a savefile a bunch of messages are generated (levelup, learning talents, etc), but they # are irrelevant, since we're loading an exiting character self.ui_view.info_message.clear_messages() # Talent toggle is highlighted when new talents are unlocked, but we don't want it to be highlighted on startup # when loading from a savefile self.ui_view.remove_highlight_from_talent_toggle() allocate_input_keys_for_abilities(game_state.player_state.abilities) new_hero_was_created = saved_player_state is None playing_scene = self.playing_scene(game_state, game_engine, world_behavior, self.ui_view, new_hero_was_created, character_file, total_time_played_on_character) return SceneTransition(playing_scene)
def run_one_frame(self, _time_passed: Millis) -> Optional[SceneTransition]: saved_player_state = self.flags.saved_player_state hero_start_level = self.flags.hero_start_level start_money = self.flags.start_money picked_hero = self.flags.picked_hero map_file_path = self.flags.map_file_path character_file = self.flags.character_file if saved_player_state: hero_from_saved_state = HeroId[saved_player_state.hero_id] if picked_hero is not None and picked_hero != hero_from_saved_state: raise Exception("Mismatch! Hero from saved state: " + str(hero_from_saved_state) + ", but picked hero: " + str(picked_hero)) picked_hero = hero_from_saved_state total_time_played_on_character = saved_player_state.total_time_played_on_character if saved_player_state else 0 ui_state = GameUiState() game_state = create_game_state_from_json_file(self.camera_size, map_file_path, picked_hero) game_engine = GameEngine(game_state, ui_state) game_state.player_state.exp_was_updated.register_observer( self.ui_view.on_player_exp_updated) game_state.player_state.talents_were_updated.register_observer( self.ui_view.on_talents_updated) game_state.player_state.notify_talent_observers( ) # Must notify the initial state game_state.player_movement_speed_was_updated.register_observer( self.ui_view.on_player_movement_speed_updated) game_state.notify_movement_speed_observers( ) # Must notify the initial state game_state.player_state.stats_were_updated.register_observer( self.ui_view.on_player_stats_updated) game_state.player_state.notify_stats_observers( ) # Must notify the initial state game_engine.talent_was_unlocked.register_observer( self.ui_view.on_talent_was_unlocked) game_state.player_state.money_was_updated.register_observer( self.ui_view.on_money_updated) game_state.player_state.notify_money_observers( ) # Must notify the initial state game_state.player_state.abilities_were_updated.register_observer( self.ui_view.on_abilities_updated) game_state.player_state.notify_ability_observers( ) # Must notify the initial state game_state.player_state.cooldowns_were_updated.register_observer( self.ui_view.on_cooldowns_updated) game_state.player_state.health_resource.value_was_updated.register_observer( self.ui_view.on_health_updated) game_state.player_state.mana_resource.value_was_updated.register_observer( self.ui_view.on_mana_updated) game_state.player_state.buffs_were_updated.register_observer( self.ui_view.on_buffs_updated) game_state.player_entity.movement_changed = Observable() game_state.player_entity.movement_changed.register_observer( play_or_stop_footstep_sounds) if map_file_path == 'resources/maps/challenge.json': world_behavior = ChallengeBehavior(self.picking_hero_scene, self.challenge_complete_scene, game_state, ui_state, game_engine, self.flags) else: world_behavior = StoryBehavior(self.victory_screen_scene, game_state, ui_state) if saved_player_state: game_engine.gain_levels(saved_player_state.level - 1) game_state.player_state.gain_exp(saved_player_state.exp) game_engine.set_item_inventory([ ItemType[item] if item else None for item in saved_player_state.items ]) game_state.player_state.consumable_inventory = ConsumableInventory( { int(slot_number): [ConsumableType[c] for c in consumables] for (slot_number, consumables ) in saved_player_state.consumables_in_slots.items() }) game_state.player_state.modify_money(saved_player_state.money) for portal in game_state.portals: if portal.portal_id.name in saved_player_state.enabled_portals: sprite = saved_player_state.enabled_portals[ portal.portal_id.name] portal.activate(Sprite[sprite]) for tier_index, option_index in enumerate( saved_player_state.talent_tier_choices): if option_index is not None: pick_talent(game_state, tier_index, option_index) else: if hero_start_level > 1: game_engine.gain_levels(hero_start_level - 1) if start_money > 0: game_state.player_state.modify_money(start_money) game_state.player_state.item_inventory.was_updated.register_observer( self.ui_view.on_inventory_updated) game_state.player_state.item_inventory.notify_observers( ) # Must notify the initial state game_state.player_state.consumable_inventory.was_updated.register_observer( self.ui_view.on_consumables_updated) game_state.player_state.consumable_inventory.notify_observers( ) # Must notify the initial state self.ui_view.update_hero(game_state.player_state.hero_id) # When loading from a savefile a bunch of messages are generated (levelup, learning talents, etc), but they # are irrelevant, since we're loading an exiting character ui_state.clear_messages() # Talent toggle is highlighted when new talents are unlocked, but we don't want it to be highlighted on startup # when loading from a savefile self.ui_view.remove_highlight_from_talent_toggle() allocate_input_keys_for_abilities(game_state.player_state.abilities) new_hero_was_created = saved_player_state is None playing_scene = self.playing_scene(game_state, game_engine, world_behavior, ui_state, self.ui_view, new_hero_was_created, character_file, total_time_played_on_character) return SceneTransition(playing_scene)
class ItemInventory: def __init__(self, slots: List[ItemInventorySlot]): self.slots = slots self.was_updated = Observable() def switch_item_slots(self, slot_1_index: int, slot_2_index: int) -> List[ItemActivationEvent]: slot_1 = self.slots[slot_1_index] slot_2 = self.slots[slot_2_index] content_1 = slot_1.item content_2 = slot_2.item events = [] is_switch_allowed = slot_2.can_contain_item( content_1) and slot_1.can_contain_item(content_2) if is_switch_allowed: if content_1: item_id_1 = slot_1.get_item_id() if slot_1.is_active() and not slot_2.is_active(): event_1 = ItemWasDeactivated(item_id_1) elif not slot_1.is_active() and slot_2.is_active(): event_1 = ItemWasActivated(item_id_1) else: event_1 = ItemActivationStateDidNotChange(item_id_1) events.append(event_1) if content_2: item_id_2 = slot_2.get_item_id() if slot_2.is_active() and not slot_1.is_active(): event_2 = ItemWasDeactivated(item_id_2) elif not slot_2.is_active() and slot_1.is_active(): event_2 = ItemWasActivated(item_id_2) else: event_2 = ItemActivationStateDidNotChange(item_id_2) events.append(event_2) slot_1.item = content_2 slot_2.item = content_1 if len(events) == 2: # If switching two items that both have active abilities, the currently active ability needs to be removed # before the new one can be activated. If we don't switch the order here, the consequence is that no ability # will be active. if isinstance(events[0], ItemWasActivated) and isinstance( events[1], ItemWasDeactivated): events = [events[1], events[0]] self.notify_observers() return events def try_switch_item_at_slot(self, slot_index: int) -> List[ItemActivationEvent]: if self.slots[slot_index].is_empty(): return [] for other_slot_index in [ s for s in range(len(self.slots)) if s != slot_index ]: events = self.switch_item_slots(slot_index, other_slot_index) if events: self.notify_observers() return events return [] def has_item_in_inventory(self, item_id: ItemId): matches = [ slot for slot in self.slots if not slot.is_empty() and slot.get_item_id() == item_id ] if len(matches) > 0: return True def lose_item_from_inventory(self, item_id: ItemId): for slot_number in range(len(self.slots)): slot = self.slots[slot_number] if not slot.is_empty() and slot.get_item_id() == item_id: self.slots[slot_number].item = None self.notify_observers() return print("WARN: item not found in inventory: " + str(item_id)) # Note: this will need to return events, if it's used for items that have effects def is_slot_empty(self, slot_index: int) -> bool: return self.slots[slot_index].is_empty() def try_add_item(self, item_id: ItemId, item_effect, item_equipment_category: ItemEquipmentCategory) \ -> Optional[ItemActivationEvent]: item_in_slot = ItemInSlot(item_id, item_effect, item_equipment_category) empty_slot_index = self._find_empty_slot_for_item(item_in_slot) if empty_slot_index is not None: slot = self.slots[empty_slot_index] slot.item = item_in_slot self.notify_observers() if slot.is_active(): return ItemWasActivated(item_id) else: return ItemActivationStateDidNotChange(item_id) return None def put_item_in_inventory_slot( self, item_id: ItemId, item_effect, item_equipment_category: ItemEquipmentCategory, slot_number: int) -> ItemActivationEvent: item_in_slot = ItemInSlot(item_id, item_effect, item_equipment_category) slot = self.slots[slot_number] if not slot.is_empty(): raise Exception("Can't put item in non-empty slot!") slot.item = item_in_slot self.notify_observers() if slot.is_active(): return ItemWasActivated(item_id) else: return ItemActivationStateDidNotChange(item_id) def _find_empty_slot_for_item(self, item: ItemInSlot) -> Optional[int]: empty_slot_indices = [ i for i in range(len(self.slots)) if self.slots[i].is_empty() and self.slots[i].can_contain_item(item) ] if empty_slot_indices: return empty_slot_indices[0] return None def remove_item_from_slot(self, slot_index: int) -> ItemActivationEvent: slot = self.slots[slot_index] if slot.is_empty(): raise Exception("Can't remove item from empty inventory slot: " + str(slot_index)) item_id = slot.get_item_id() slot.item = None self.notify_observers() if slot.is_active(): return ItemWasDeactivated(item_id) return ItemActivationStateDidNotChange(item_id) def clear(self) -> List[ItemActivationEvent]: events = [] for slot in self.slots: if not slot.is_empty() and slot.is_active(): active_item_id = slot.get_item_id() events.append(ItemWasDeactivated(active_item_id)) slot.item = None self.notify_observers() return events def get_all_active_item_effects(self) -> List[Any]: return [ slot.item.item_effect for slot in self.slots if slot.is_active() and not slot.is_empty() ] def notify_observers(self): self.was_updated.notify(self.slots) def __repr__(self): return str(self.slots)
def __init__(self, slots: Dict[int, List[ConsumableType]]): self.consumables_in_slots: Dict[int, List[ConsumableType]] = slots self.capacity_per_slot = 2 self.was_updated = Observable()