def __init__(self, session, building, ship=None, build_related=None): super(BuildingTool, self).__init__(session) assert not (ship and build_related) self.renderer = self.session.view.renderer['InstanceRenderer'] self.ship = ship self._class = building self.__init_selectable_component() self.buildings = [] # list of PossibleBuild objs self.buildings_action_set_ids = [] # list action set ids of list above self.buildings_fife_instances = {} # fife instances of possible builds self.buildings_missing_resources = { } # missing resources for possible builds self.rotation = 45 + random.randint(0, 3) * 90 self.start_point, self.end_point = None, None self.last_change_listener = None self._transparencified_instances = set( ) # fife instances modified for transparency self._buildable_tiles = set() # tiles marked as buildable self._related_buildings = set() # buildings highlighted as related self._highlighted_buildings = set( ) # related buildings highlighted when preview is near it self._build_logic = None self._related_buildings_selected_tiles = frozenset( ) # highlights w.r.t. related buildings if self.ship is not None: self._build_logic = ShipBuildingToolLogic(ship) elif build_related is not None: self._build_logic = BuildRelatedBuildingToolLogic( self, weakref.ref(build_related)) else: self._build_logic = SettlementBuildingToolLogic(self) self.load_gui() self.__class__.gui.show() self.session.ingame_gui.minimap_to_front() self.session.gui.on_escape = self.on_escape self.highlight_buildable() WorldObjectDeleted.subscribe(self._on_worldobject_deleted) SettlementInventoryUpdated.subscribe(self.update_preview) PlayerInventoryUpdated.subscribe(self.update_preview)
def __init__(self, session, building, ship=None, build_related=None): super(BuildingTool, self).__init__(session) assert not (ship and build_related) self.renderer = self.session.view.renderer['InstanceRenderer'] self.ship = ship self._class = building self.__init_selectable_component() self.buildings = [] # list of PossibleBuild objs self.buildings_action_set_ids = [] # list action set ids of list above self.buildings_fife_instances = {} # fife instances of possible builds self.buildings_missing_resources = {} # missing resources for possible builds self.rotation = 45 + random.randint(0, 3)*90 self.start_point, self.end_point = None, None self.last_change_listener = None self._transparencified_instances = set() # fife instances modified for transparency self._buildable_tiles = set() # tiles marked as buildable self._related_buildings = set() # buildings highlighted as related self._highlighted_buildings = set() # related buildings highlighted when preview is near it self._build_logic = None self._related_buildings_selected_tiles = frozenset() # highlights w.r.t. related buildings if self.ship is not None: self._build_logic = ShipBuildingToolLogic(ship) elif build_related is not None: self._build_logic = BuildRelatedBuildingToolLogic(self, weakref.ref(build_related) ) else: self._build_logic = SettlementBuildingToolLogic(self) self.load_gui() self.__class__.gui.show() self.session.ingame_gui.minimap_to_front() self.session.gui.on_escape = self.on_escape self.highlight_buildable() WorldObjectDeleted.subscribe(self._on_worldobject_deleted) SettlementInventoryUpdated.subscribe(self.update_preview) PlayerInventoryUpdated.subscribe(self.update_preview)
class BuildingTool(NavigationTool, previewhandler.PreviewHandler): """Represents a dangling tool after a building was selected from the list. Builder visualizes if and why a building can not be built under the cursor position. @param building: selected building type" @param ship: If building from a ship, restrict to range of ship The building tool has been under heavy development for several years, it's a collection of random artifacts that used to have a purpose once. Terminology: - Related buildings: Buildings lower in the hiearchy, needed by current building to operate (tree when building lumberjack) - Inversely related building: lumberjack for tree. Need to show its range to place building, it must be in range. - Building instances/fife instances: the image of a building, that is dragged around. Main features: - Display tab to the right, showing build preview icon and rotation button (draw_gui(), load_gui()) - Show buildable ground (highlight buildable) as well as ranges of inversely related buildings - This also is called for tiles that need to be recolored, other highlights sometimes draw over tiles, then this is called again to redo the original coloring. - Catch mouse events and handle preview on map: - Get tentative fife instances for buildings, draw them colored according to buildability - Check for resources missing for build - Making surrounding of preview transparent, so you see where you are building in a forest - Highlight related buildings, that are in range - Draw building range and highlight related buildings that are in range in this position (_color_preview_building) - Initiate actual build (do_build) - Clean up coloring, possibly end build mode - Several buildability logics, strategy pattern via self._build_logic. Interaction sequence: - Init, comprises mainly of gui init and highlight_buildable - Update, which is mainly controlled by preview_build - Update highlights related to build - Transparency - Inversely related buildings in range (highlight_related_buildings) - Related buildings in range (_color_preview_build) - Set new instances - During this time, don't touch anything set by highlight_buildable, or restore it later - End, possibly do_build and on_escape """ ####################################################### #log = logging.getLogger("gui.buildingtool") #buildable_color = (255, 255, 255) #not_buildable_color = (255, 0, 0) #related_building_color = (0, 192, 0) #related_building_outline = (16, 228, 16, 2) #nearby_objects_radius = 4 # archive the last roads built, for possible user notification #_last_road_built = [] #send_hover_instances_update = False #gui = None # share gui between instances ####################################################### def __init__(self, session, building, ship=None, build_related=None): super(BuildingTool, self).__init__(session) assert not (ship and build_related) self.renderer = self.session.view.renderer['InstanceRenderer'] self.ship = ship self._class = building self.__init_selectable_component() self.buildings = [] # list of PossibleBuild objs self.buildings_action_set_ids = [] # list action set ids of list above self.buildings_fife_instances = {} # fife instances of possible builds self.buildings_missing_resources = { } # missing resources for possible builds self.rotation = 45 + random.randint(0, 3) * 90 self.start_point, self.end_point = None, None self.last_change_listener = None self._transparencified_instances = set( ) # fife instances modified for transparency self._buildable_tiles = set() # tiles marked as buildable self._related_buildings = set() # buildings highlighted as related self._highlighted_buildings = set( ) # related buildings highlighted when preview is near it self._build_logic = None self._related_buildings_selected_tiles = frozenset( ) # highlights w.r.t. related buildings if self.ship is not None: self._build_logic = ShipBuildingToolLogic(ship) elif build_related is not None: self._build_logic = BuildRelatedBuildingToolLogic( self, weakref.ref(build_related)) else: self._build_logic = SettlementBuildingToolLogic(self) self.load_gui() self.__class__.gui.show() self.session.ingame_gui.minimap_to_front() self.session.gui.on_escape = self.on_escape self.highlight_buildable() WorldObjectDeleted.subscribe(self._on_worldobject_deleted) SettlementInventoryUpdated.subscribe(self.update_preview) PlayerInventoryUpdated.subscribe(self.update_preview) def __init_selectable_component(self): self.selectable_comp = SelectableBuildingComponent try: template = self._class.get_component_template(SelectableComponent) self.selectable_comp = SelectableComponent.get_instance(template) except KeyError: pass def remove(self): self.session.ingame_gui.resource_overview.close_construction_mode() WorldObjectDeleted.unsubscribe(self._on_worldobject_deleted) self._remove_listeners() self._remove_building_instances() self._remove_coloring() self._build_logic.remove(self.session) self._buildable_tiles = None self._transparencified_instances = None self._related_buildings_selected_tiles = None self._related_buildings = None self._highlighted_buildings = None self._build_logic = None self.buildings = None if self.__class__.gui is not None: self.__class__.gui.hide() ExtScheduler().rem_all_classinst_calls(self) SettlementInventoryUpdated.discard(self.update_preview) PlayerInventoryUpdated.discard(self.update_preview) super(BuildingTool, self).remove() def _on_worldobject_deleted(self, message): # remove references to this object self._related_buildings.discard(message.sender) self._transparencified_instances = \ set( i for i in self._transparencified_instances if i() is not None and int(i().getId()) != message.worldid ) check_building = lambda b: b.worldid != message.worldid self._highlighted_buildings = set( tup for tup in self._highlighted_buildings if check_building(tup[0])) self._related_buildings = set( filter(check_building, self._related_buildings)) def on_escape(self): self._build_logic.on_escape(self.session) if self.__class__.gui is not None: self.__class__.gui.hide() self.session.set_cursor() # will call remove() def mouseMoved(self, evt): self.log.debug("BuildingTool mouseMoved") super(BuildingTool, self).mouseMoved(evt) point = self.get_world_location(evt) if self.start_point != point: self.start_point = point self._check_update_preview(point) evt.consume() def mousePressed(self, evt): self.log.debug("BuildingTool mousePressed") if evt.isConsumedByWidgets(): super(BuildingTool, self).mousePressed(evt) return if evt.getButton() == fife.MouseEvent.RIGHT: self.on_escape() elif evt.getButton() == fife.MouseEvent.LEFT: pass else: super(BuildingTool, self).mousePressed(evt) return evt.consume() def mouseDragged(self, evt): self.log.debug("BuildingTool mouseDragged") super(BuildingTool, self).mouseDragged(evt) point = self.get_world_location(evt) if self.start_point is not None: self._check_update_preview(point) evt.consume() def mouseReleased(self, evt): """Actually build.""" self.log.debug("BuildingTool mouseReleased") if evt.isConsumedByWidgets(): super(BuildingTool, self).mouseReleased(evt) elif evt.getButton() == fife.MouseEvent.LEFT: point = self.get_world_location(evt) # check if position has changed with this event and update everything self._check_update_preview(point) # actually do the build changed_tiles = self.do_build() found_buildable = bool(changed_tiles) if found_buildable: PlaySound("build").execute(self.session, local=True) # HACK: users sometimes don't realise that roads can be dragged # check if 3 roads have been built within 1.2 seconds, and display # a hint in case if self._class.class_package == 'path': import time now = time.time() BuildingTool._last_road_built.append(now) if len(BuildingTool._last_road_built) > 2: if (now - BuildingTool._last_road_built[-3]) < 1.2: self.session.ingame_gui.message_widget.add( point=None, string_id="DRAG_ROADS_HINT") # don't display hint multiple times at the same build situation BuildingTool._last_road_built = [] BuildingTool._last_road_built = BuildingTool._last_road_built[ -3:] # check how to continue: either build again or escape shift = evt.isShiftPressed( ) or horizons.globals.fife.get_uh_setting('UninterruptedBuilding') if ((shift and not self._class.id == BUILDINGS.WAREHOUSE) or not found_buildable or self._class.class_package == 'path'): # build once more self._restore_transparencified_instances() self.highlight_buildable(changed_tiles) self.start_point = point self._build_logic.continue_build() self.preview_build(point, point) else: self.on_escape() evt.consume() elif evt.getButton() != fife.MouseEvent.RIGHT: super(BuildingTool, self).mouseReleased(evt) def do_build(self): """Actually builds the previews @return a set of tiles where buildings have really been built""" changed_tiles = set() # actually do the build and build preparations for i, building in enumerate(self.buildings): # remove fife instance, the building will create a new one. # Check if there is a matching fife instance, could be missing # in case of trees, which are hidden if not buildable if building in self.buildings_fife_instances: fife_instance = self.buildings_fife_instances.pop(building) self.renderer.removeColored(fife_instance) self.renderer.removeOutlined(fife_instance) fife_instance.getLocationRef().getLayer().deleteInstance( fife_instance) if building.buildable: island = self.session.world.get_island( building.position.origin) for position in building.position: tile = island.get_tile(position) if tile in self._buildable_tiles: # for some kind of buildabilities, not every coord of the # building is buildable (e.g. fisher: only coastline is marked # as buildable). For those tiles, that are not buildable, # we don't need to do anything. self._buildable_tiles.remove(tile) self.renderer.removeColored(tile._instance) changed_tiles.add(tile) self._remove_listeners( ) # Remove changelisteners for update_preview # create the command and execute it cmd = Build( building=self._class, x=building.position.origin.x, y=building.position.origin.y, rotation=building.rotation, island=island, settlement=self.session.world.get_settlement( building.position.origin), ship=self.ship, tearset=building.tearset, action_set_id=self.buildings_action_set_ids[i], ) cmd.execute(self.session) else: if len(self.buildings ) == 1: # only give messages for single bulds # first, buildable reasons such as grounds # second, resources if building.problem is not None: msg = building.problem[1] self.session.ingame_gui.message_widget.add_custom( point=building.position.origin, messagetext=msg) # check whether to issue a missing res notification # we need the localized resource name here elif building in self.buildings_missing_resources: res_name = self.session.db.get_res_name( self.buildings_missing_resources[building]) self.session.ingame_gui.message_widget.add( point=building.position.origin, string_id='NEED_MORE_RES', message_dict={'resource': res_name}) self.buildings = [] self.buildings_action_set_ids = [] return changed_tiles def _check_update_preview(self, end_point): """Used internally if the end_point changes""" if self.end_point != end_point: self.end_point = end_point self.update_preview() def _remove_listeners(self): """Resets the ChangeListener for update_preview.""" if self.last_change_listener is not None: if self.last_change_listener.has_change_listener( self.force_update): self.last_change_listener.remove_change_listener( self.force_update) if self.last_change_listener.has_change_listener( self.highlight_buildable): self.last_change_listener.remove_change_listener( self.highlight_buildable) self._build_logic.remove_change_listener(self.last_change_listener, self) self.last_change_listener = None def _add_listeners(self, instance): if self.last_change_listener != instance: self._remove_listeners() self.last_change_listener = instance if self.last_change_listener is not None: self._build_logic.add_change_listener( self.last_change_listener, self) def force_update(self): self.update_preview(force=True) def update_preview(self, force=False): """Used as callback method""" if self.start_point is not None: end_point = self.end_point or self.start_point self.preview_build(self.start_point, end_point, force=force) def _rotate(self, degrees): self.rotation = (self.rotation + degrees) % 360 self.log.debug("BuildingTool: Building rotation now: %s", self.rotation) self.update_preview() self.draw_gui() def rotate_left(self): self._rotate(degrees=90) def rotate_right(self): self._rotate(degrees=270)
class BuildingTool(NavigationTool, previewhandler.PreviewHandler): """Represents a dangling tool after a building was selected from the list. Builder visualizes if and why a building can not be built under the cursor position. @param building: selected building type" @param ship: If building from a ship, restrict to range of ship The building tool has been under heavy development for several years, it's a collection of random artifacts that used to have a purpose once. Terminology: - Related buildings: Buildings lower in the hiearchy, needed by current building to operate (tree when building lumberjack) - Inversely related building: lumberjack for tree. Need to show its range to place building, it must be in range. - Building instances/fife instances: the image of a building, that is dragged around. Main features: - Display tab to the right, showing build preview icon and rotation button (draw_gui(), load_gui()) - Show buildable ground (highlight buildable) as well as ranges of inversely related buildings - This also is called for tiles that need to be recolored, other highlights sometimes draw over tiles, then this is called again to redo the original coloring. - Catch mouse events and handle preview on map: - Get tentative fife instances for buildings, draw them colored according to buildability - Check for resources missing for build - Making surrounding of preview transparent, so you see where you are building in a forest - Highlight related buildings, that are in range - Draw building range and highlight related buildings that are in range in this position (_color_preview_building) - Initiate actual build (do_build) - Clean up coloring, possibly end build mode - Several buildability logics, strategy pattern via self._build_logic. Interaction sequence: - Init, comprises mainly of gui init and highlight_buildable - Update, which is mainly controlled by preview_build - Update highlights related to build - Transparency - Inversely related buildings in range (highlight_related_buildings) - Related buildings in range (_color_preview_build) - Set new instances - During this time, don't touch anything set by highlight_buildable, or restore it later - End, possibly do_build and on_escape """ ####################################################### #log = logging.getLogger("gui.buildingtool") #buildable_color = (255, 255, 255) #not_buildable_color = (255, 0, 0) #related_building_color = (0, 192, 0) #related_building_outline = (16, 228, 16, 2) #nearby_objects_radius = 4 # archive the last roads built, for possible user notification #_last_road_built = [] #send_hover_instances_update = False #gui = None # share gui between instances ####################################################### def __init__(self, session, building, ship=None, build_related=None): super(BuildingTool, self).__init__(session) assert not (ship and build_related) self.renderer = self.session.view.renderer['InstanceRenderer'] self.ship = ship self._class = building self.__init_selectable_component() self.buildings = [] # list of PossibleBuild objs self.buildings_action_set_ids = [] # list action set ids of list above self.buildings_fife_instances = {} # fife instances of possible builds self.buildings_missing_resources = {} # missing resources for possible builds self.rotation = 45 + random.randint(0, 3)*90 self.start_point, self.end_point = None, None self.last_change_listener = None self._transparencified_instances = set() # fife instances modified for transparency self._buildable_tiles = set() # tiles marked as buildable self._related_buildings = set() # buildings highlighted as related self._highlighted_buildings = set() # related buildings highlighted when preview is near it self._build_logic = None self._related_buildings_selected_tiles = frozenset() # highlights w.r.t. related buildings if self.ship is not None: self._build_logic = ShipBuildingToolLogic(ship) elif build_related is not None: self._build_logic = BuildRelatedBuildingToolLogic(self, weakref.ref(build_related) ) else: self._build_logic = SettlementBuildingToolLogic(self) self.load_gui() self.__class__.gui.show() self.session.ingame_gui.minimap_to_front() self.session.gui.on_escape = self.on_escape self.highlight_buildable() WorldObjectDeleted.subscribe(self._on_worldobject_deleted) SettlementInventoryUpdated.subscribe(self.update_preview) PlayerInventoryUpdated.subscribe(self.update_preview) def __init_selectable_component(self): self.selectable_comp = SelectableBuildingComponent try: template = self._class.get_component_template(SelectableComponent) self.selectable_comp = SelectableComponent.get_instance(template) except KeyError: pass def remove(self): self.session.ingame_gui.resource_overview.close_construction_mode() WorldObjectDeleted.unsubscribe(self._on_worldobject_deleted) self._remove_listeners() self._remove_building_instances() self._remove_coloring() self._build_logic.remove(self.session) self._buildable_tiles = None self._transparencified_instances = None self._related_buildings_selected_tiles = None self._related_buildings = None self._highlighted_buildings = None self._build_logic = None self.buildings = None if self.__class__.gui is not None: self.__class__.gui.hide() ExtScheduler().rem_all_classinst_calls(self) SettlementInventoryUpdated.discard(self.update_preview) PlayerInventoryUpdated.discard(self.update_preview) super(BuildingTool, self).remove() def _on_worldobject_deleted(self, message): # remove references to this object self._related_buildings.discard(message.sender) self._transparencified_instances = \ set( i for i in self._transparencified_instances if i() is not None and int(i().getId()) != message.worldid ) check_building = lambda b : b.worldid != message.worldid self._highlighted_buildings = set( tup for tup in self._highlighted_buildings if check_building(tup[0]) ) self._related_buildings = set( filter(check_building, self._related_buildings) ) def on_escape(self): self._build_logic.on_escape(self.session) if self.__class__.gui is not None: self.__class__.gui.hide() self.session.set_cursor() # will call remove() def mouseMoved(self, evt): self.log.debug("BuildingTool mouseMoved") super(BuildingTool, self).mouseMoved(evt) point = self.get_world_location(evt) if self.start_point != point: self.start_point = point self._check_update_preview(point) evt.consume() def mousePressed(self, evt): self.log.debug("BuildingTool mousePressed") if evt.isConsumedByWidgets(): super(BuildingTool, self).mousePressed(evt) return if evt.getButton() == fife.MouseEvent.RIGHT: self.on_escape() elif evt.getButton() == fife.MouseEvent.LEFT: pass else: super(BuildingTool, self).mousePressed(evt) return evt.consume() def mouseDragged(self, evt): self.log.debug("BuildingTool mouseDragged") super(BuildingTool, self).mouseDragged(evt) point = self.get_world_location(evt) if self.start_point is not None: self._check_update_preview(point) evt.consume() def mouseReleased(self, evt): """Actually build.""" self.log.debug("BuildingTool mouseReleased") if evt.isConsumedByWidgets(): super(BuildingTool, self).mouseReleased(evt) elif evt.getButton() == fife.MouseEvent.LEFT: point = self.get_world_location(evt) # check if position has changed with this event and update everything self._check_update_preview(point) # actually do the build changed_tiles = self.do_build() found_buildable = bool(changed_tiles) if found_buildable: PlaySound("build").execute(self.session, local=True) # HACK: users sometimes don't realise that roads can be dragged # check if 3 roads have been built within 1.2 seconds, and display # a hint in case if self._class.class_package == 'path': import time now = time.time() BuildingTool._last_road_built.append(now) if len(BuildingTool._last_road_built) > 2: if (now - BuildingTool._last_road_built[-3]) < 1.2: self.session.ingame_gui.message_widget.add(point=None, string_id="DRAG_ROADS_HINT") # don't display hint multiple times at the same build situation BuildingTool._last_road_built = [] BuildingTool._last_road_built = BuildingTool._last_road_built[-3:] # check how to continue: either build again or escape shift = evt.isShiftPressed() or horizons.globals.fife.get_uh_setting('UninterruptedBuilding') if ((shift and not self._class.id == BUILDINGS.WAREHOUSE) or not found_buildable or self._class.class_package == 'path'): # build once more self._restore_transparencified_instances() self.highlight_buildable(changed_tiles) self.start_point = point self._build_logic.continue_build() self.preview_build(point, point) else: self.on_escape() evt.consume() elif evt.getButton() != fife.MouseEvent.RIGHT: super(BuildingTool, self).mouseReleased(evt) def do_build(self): """Actually builds the previews @return a set of tiles where buildings have really been built""" changed_tiles = set() # actually do the build and build preparations for i, building in enumerate(self.buildings): # remove fife instance, the building will create a new one. # Check if there is a matching fife instance, could be missing # in case of trees, which are hidden if not buildable if building in self.buildings_fife_instances: fife_instance = self.buildings_fife_instances.pop(building) self.renderer.removeColored(fife_instance) self.renderer.removeOutlined(fife_instance) fife_instance.getLocationRef().getLayer().deleteInstance(fife_instance) if building.buildable: island = self.session.world.get_island(building.position.origin) for position in building.position: tile = island.get_tile(position) if tile in self._buildable_tiles: # for some kind of buildabilities, not every coord of the # building is buildable (e.g. fisher: only coastline is marked # as buildable). For those tiles, that are not buildable, # we don't need to do anything. self._buildable_tiles.remove(tile) self.renderer.removeColored(tile._instance) changed_tiles.add(tile) self._remove_listeners() # Remove changelisteners for update_preview # create the command and execute it cmd = Build(building=self._class, x=building.position.origin.x, y=building.position.origin.y, rotation=building.rotation, island=island, settlement=self.session.world.get_settlement(building.position.origin), ship=self.ship, tearset=building.tearset, action_set_id=self.buildings_action_set_ids[i], ) cmd.execute(self.session) else: if len(self.buildings) == 1: # only give messages for single bulds # first, buildable reasons such as grounds # second, resources if building.problem is not None: msg = building.problem[1] self.session.ingame_gui.message_widget.add_custom( point=building.position.origin, messagetext=msg) # check whether to issue a missing res notification # we need the localized resource name here elif building in self.buildings_missing_resources: res_name = self.session.db.get_res_name( self.buildings_missing_resources[building] ) self.session.ingame_gui.message_widget.add( point=building.position.origin, string_id='NEED_MORE_RES', message_dict={'resource' : res_name}) self.buildings = [] self.buildings_action_set_ids = [] return changed_tiles def _check_update_preview(self, end_point): """Used internally if the end_point changes""" if self.end_point != end_point: self.end_point = end_point self.update_preview() def _remove_listeners(self): """Resets the ChangeListener for update_preview.""" if self.last_change_listener is not None: if self.last_change_listener.has_change_listener(self.force_update): self.last_change_listener.remove_change_listener(self.force_update) if self.last_change_listener.has_change_listener(self.highlight_buildable): self.last_change_listener.remove_change_listener(self.highlight_buildable) self._build_logic.remove_change_listener(self.last_change_listener, self) self.last_change_listener = None def _add_listeners(self, instance): if self.last_change_listener != instance: self._remove_listeners() self.last_change_listener = instance if self.last_change_listener is not None: self._build_logic.add_change_listener(self.last_change_listener, self) def force_update(self): self.update_preview(force=True) def update_preview(self, force=False): """Used as callback method""" if self.start_point is not None: end_point = self.end_point or self.start_point self.preview_build(self.start_point, end_point, force=force) def _rotate(self, degrees): self.rotation = (self.rotation + degrees) % 360 self.log.debug("BuildingTool: Building rotation now: %s", self.rotation) self.update_preview() self.draw_gui() def rotate_left(self): self._rotate(degrees=90) def rotate_right(self): self._rotate(degrees=270)