Package horizons :: Package gui :: Package mousetools :: Module buildingtool
[hide private]
[frames] | no frames]

Source Code for Module horizons.gui.mousetools.buildingtool

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import logging 
 23  import random 
 24  import weakref 
 25  from typing import TYPE_CHECKING, List 
 26   
 27  from fife import fife 
 28   
 29  import horizons.globals 
 30  from horizons.command.building import Build 
 31  from horizons.command.sounds import PlaySound 
 32  from horizons.component.selectablecomponent import SelectableBuildingComponent, SelectableComponent 
 33  from horizons.constants import BUILDINGS, GFX 
 34  from horizons.entities import Entities 
 35  from horizons.extscheduler import ExtScheduler 
 36  from horizons.gui.mousetools.navigationtool import NavigationTool 
 37  from horizons.gui.util import load_uh_widget 
 38  from horizons.i18n import gettext as T 
 39  from horizons.messaging import ( 
 40          PlayerInventoryUpdated, SettlementInventoryUpdated, SettlementRangeChanged, WorldObjectDeleted) 
 41  from horizons.util.loaders.actionsetloader import ActionSetLoader 
 42  from horizons.util.shapes import Point 
 43  from horizons.util.worldobject import WorldObject 
 44   
 45  if TYPE_CHECKING: 
 46          from fife.extensions.pychan.widgets import Widget 
 47   
 48   
49 -class BuildingTool(NavigationTool):
50 """Represents a dangling tool after a building was selected from the list. 51 Builder visualizes if and why a building can not be built under the cursor position. 52 @param building: selected building type" 53 @param ship: If building from a ship, restrict to range of ship 54 55 The building tool has been under heavy development for several years, it's a collection 56 of random artifacts that used to have a purpose once. 57 58 Terminology: 59 - Related buildings: Buildings lower in the hierarchy, needed by current building to operate (tree when building lumberjack) 60 - Inversely related building: lumberjack for tree. Need to show its range to place building, it must be in range. 61 - Building instances/fife instances: the image of a building, that is dragged around. 62 63 Main features: 64 - Display tab to the right, showing build preview icon and rotation button (draw_gui(), load_gui()) 65 - Show buildable ground (highlight buildable) as well as ranges of inversely related buildings 66 - This also is called for tiles that need to be recolored, other highlights sometimes draw over 67 tiles, then this is called again to redo the original coloring. 68 - Catch mouse events and handle preview on map: 69 - Get tentative fife instances for buildings, draw them colored according to buildability 70 - Check for resources missing for build 71 - Making surrounding of preview transparent, so you see where you are building in a forest 72 - Highlight related buildings, that are in range 73 - Draw building range and highlight related buildings that are in range in this position (_color_preview_building) 74 - Initiate actual build (do_build) 75 - Clean up coloring, possibly end build mode 76 - Several buildability logics, strategy pattern via self._build_logic. 77 78 Interaction sequence: 79 - Init, comprises mainly of gui init and highlight_buildable 80 - Update, which is mainly controlled by preview_build 81 - Update highlights related to build 82 - Transparency 83 - Inversely related buildings in range (highlight_related_buildings) 84 - Related buildings in range (_color_preview_build) 85 - Set new instances 86 - During this time, don't touch anything set by highlight_buildable, or restore it later 87 - End, possibly do_build and on_escape 88 """ 89 log = logging.getLogger("gui.buildingtool") 90 91 buildable_color = (255, 255, 255) 92 not_buildable_color = (255, 0, 0, 160) 93 related_building_color = (0, 192, 0, 160) 94 related_building_outline = (32, 192, 32, 3) 95 nearby_objects_radius = 4 96 97 # archive the last roads built, for possible user notification 98 _last_road_built = [] # type: List[int] 99 100 send_hover_instances_update = False 101 102 # share gui between instances 103 gui = None # type: Widget 104
105 - def __init__(self, session, building, ship=None, build_related=None):
106 super().__init__(session) 107 assert not (ship and build_related) 108 self.renderer = self.session.view.renderer['InstanceRenderer'] 109 self.ship = ship 110 self._class = building 111 self.__init_selectable_component() 112 self.buildings = [] # list of PossibleBuild objs 113 self.buildings_action_set_ids = [] # list action set ids of list above 114 self.buildings_fife_instances = {} # fife instances of possible builds 115 self.buildings_missing_resources = {} # missing resources for possible builds 116 self.rotation = 45 + random.randint(0, 3) * 90 117 self.start_point, self.end_point = None, None 118 self.last_change_listener = None 119 self._transparencified_instances = set() # fife instances modified for transparency 120 self._buildable_tiles = set() # tiles marked as buildable 121 self._related_buildings = set() # buildings highlighted as related 122 self._highlighted_buildings = set() # related buildings highlighted when preview is near it 123 self._build_logic = None 124 self._related_buildings_selected_tiles = frozenset() # highlights w.r.t. related buildings 125 if self.ship is not None: 126 self._build_logic = ShipBuildingToolLogic(ship) 127 elif build_related is not None: 128 self._build_logic = BuildRelatedBuildingToolLogic(self, weakref.ref(build_related)) 129 else: 130 self._build_logic = SettlementBuildingToolLogic(self) 131 132 self.load_gui() 133 self.__class__.gui.show() 134 self.session.ingame_gui.minimap_to_front() 135 136 self.highlight_buildable() 137 WorldObjectDeleted.subscribe(self._on_worldobject_deleted) 138 139 SettlementInventoryUpdated.subscribe(self.update_preview) 140 PlayerInventoryUpdated.subscribe(self.update_preview)
141
143 self.selectable_comp = SelectableBuildingComponent 144 try: 145 template = self._class.get_component_template(SelectableComponent) 146 self.selectable_comp = SelectableComponent.get_instance(template) 147 except KeyError: 148 pass
149
150 - def highlight_buildable(self, tiles_to_check=None, new_buildings=True):
151 """Highlights all buildable tiles and select buildings that are inversely related in order to show their range. 152 @param tiles_to_check: list of tiles to check for coloring. 153 @param new_buildings: Set to True if you have set tiles_to_check and there are new buildings. An internal structure for optimization will be amended.""" 154 self._build_logic.highlight_buildable(self, tiles_to_check) 155 156 # Also distinguish inversely related buildings (lumberjack for tree). 157 # Highlight their range at all times. 158 # (There is another similar highlight, but it only marks building when 159 # the current build preview is in its range) 160 related = self.session.db.get_inverse_related_building_ids(self._class.id) 161 162 # If the current buildings has related buildings, also show other buildings 163 # of this class. You usually don't want overlapping ranges of e.g. lumberjacks. 164 if self._class.id in self.session.db.get_buildings_with_related_buildings() and \ 165 self._class.id != BUILDINGS.RESIDENTIAL: 166 # TODO: generalize settler class exclusion, e.g. when refactoring it into components 167 168 related = related + [self._class.id] # don't += on retrieved data from db 169 170 related = frozenset(related) 171 172 renderer = self.session.view.renderer['InstanceRenderer'] 173 if tiles_to_check is None or new_buildings: # first run, check all 174 buildings_to_select = [buildings_to_select 175 for settlement in self.session.world.settlements 176 if settlement.owner.is_local_player 177 for bid in related 178 for buildings_to_select in settlement.buildings_by_id[bid]] 179 180 tiles = self.selectable_comp.select_many(buildings_to_select, renderer) 181 self._related_buildings_selected_tiles = frozenset(tiles) 182 else: # we don't need to check all 183 # duplicates filtered later 184 buildings_to_select = [tile.object for tile in tiles_to_check if 185 tile.object is not None and tile.object.id in related] 186 for tile in tiles_to_check: 187 # check if we need to recolor the tiles 188 if tile in self._related_buildings_selected_tiles: 189 self.selectable_comp._add_selected_tile(tile, renderer, remember=False) 190 191 for building in buildings_to_select: 192 self._related_buildings.add(building)
193
194 - def _color_buildable_tile(self, tile):
195 self._buildable_tiles.add(tile) # it's a set, so duplicates are handled 196 self.renderer.addColored(tile._instance, *self.buildable_color)
197
198 - def remove(self):
199 self.session.ingame_gui.resource_overview.close_construction_mode() 200 WorldObjectDeleted.unsubscribe(self._on_worldobject_deleted) 201 self._remove_listeners() 202 self._remove_building_instances() 203 self._remove_coloring() 204 self._build_logic.remove(self.session) 205 self._buildable_tiles = None 206 self._transparencified_instances = None 207 self._related_buildings_selected_tiles = None 208 self._related_buildings = None 209 self._highlighted_buildings = None 210 self._build_logic = None 211 self.buildings = None 212 if self.__class__.gui is not None: 213 self.__class__.gui.hide() 214 ExtScheduler().rem_all_classinst_calls(self) 215 SettlementInventoryUpdated.discard(self.update_preview) 216 PlayerInventoryUpdated.discard(self.update_preview) 217 super().remove()
218
219 - def _on_worldobject_deleted(self, message):
220 # remove references to this object 221 self._related_buildings.discard(message.sender) 222 self._transparencified_instances = \ 223 set(i for i in self._transparencified_instances if 224 i() is not None and int(i().getId()) != message.worldid) 225 check_building = lambda b: b.worldid != message.worldid 226 self._highlighted_buildings = set(tup for tup in self._highlighted_buildings if check_building(tup[0])) 227 self._related_buildings = set(filter(check_building, self._related_buildings))
228
229 - def load_gui(self):
230 if self.__class__.gui is None: 231 self.__class__.gui = load_uh_widget("place_building.xml") 232 self.__class__.gui.position_technique = "right-1:top+157" 233 self.__class__.gui.mapEvents({"rotate_left": self.rotate_left, 234 "rotate_right": self.rotate_right}) 235 # set translated building name in gui 236 self.__class__.gui.findChild(name='headline').text = T('Build {building}').format(building=T(self._class.name)) 237 self.__class__.gui.findChild(name='running_costs').text = str(self._class.running_costs) 238 head_box = self.__class__.gui.findChild(name='head_box') 239 head_box.adaptLayout() # recalculates size of new content 240 # calculate and set new center 241 new_x = max(25, (self.__class__.gui.size[0] // 2) - (head_box.size[0] // 2)) 242 head_box.position = (new_x, head_box.position[1]) 243 head_box.adaptLayout() 244 self.draw_gui()
245
246 - def draw_gui(self):
247 if not hasattr(self, "action_set"): 248 try: 249 level = self._class.default_level_on_build 250 except AttributeError: 251 level = self.session.world.player.settler_level 252 action_set = self._class.get_random_action_set(level=level) 253 action_sets = ActionSetLoader.get_sets() 254 for action_option in ['idle', 'idle_full', 'single', 'abcd']: 255 if action_option in action_sets[action_set]: 256 action = action_option 257 break 258 else: # If no idle, idle_full or abcd animation found, use the first you find 259 action = list(action_sets[action_set].keys())[0] 260 rotation = (self.rotation + int(self.session.view.cam.getRotation()) - 45) % 360 261 image = sorted(action_sets[action_set][action][rotation].keys())[0] 262 if GFX.USE_ATLASES: 263 # Make sure the preview is loaded 264 horizons.globals.fife.animationloader.load_image(image, action_set, action, rotation) 265 building_icon = self.gui.findChild(name='building') 266 loaded_image = horizons.globals.fife.imagemanager.load(image) 267 building_icon.image = fife.GuiImage(loaded_image) 268 width = loaded_image.getWidth() 269 # TODO: Remove hardcoded 220 270 max_width = 220 271 if width > max_width: 272 height = loaded_image.getHeight() 273 size = (max_width, (height * max_width) // width) 274 building_icon.max_size = building_icon.min_size = building_icon.size = size 275 # TODO: Remove hardcoded 70 276 gui_x, gui_y = self.__class__.gui.size 277 icon_x, icon_y = building_icon.size 278 building_icon.position = (gui_x // 2 - icon_x // 2, 279 gui_y // 2 - icon_y // 2 - 70) 280 self.__class__.gui.adaptLayout()
281
282 - def preview_build(self, point1, point2, force=False):
283 """Display buildings as preview if build requirements are met""" 284 #self.session.view.renderer['InstanceRenderer'].removeAllColored() 285 self.log.debug("BuildingTool: preview build at %s, %s", point1, point2) 286 new_buildings = self._class.check_build_line(self.session, point1, point2, 287 rotation=self.rotation, ship=self.ship) 288 # optimization: If only one building is in the preview and the position hasn't changed 289 # => don't preview. Otherwise the preview is redrawn on every mouse move 290 if not force and len(new_buildings) == len(self.buildings) == 1 and \ 291 new_buildings[0] == self.buildings[0]: 292 return # we don't want to redo the preview 293 294 # remove old fife instances and coloring 295 self._remove_building_instances() 296 297 # get new ones 298 self.buildings = new_buildings 299 # resize list of action set ids to new buildings 300 self.buildings_action_set_ids = self.buildings_action_set_ids + ([None] * (len(self.buildings) - len(self.buildings_action_set_ids))) 301 self.buildings_action_set_ids = self.buildings_action_set_ids[: len(self.buildings)] 302 # delete old infos 303 self.buildings_fife_instances.clear() 304 self.buildings_missing_resources.clear() 305 306 settlement = None # init here so we can access it below loop 307 needed_resources = {} 308 # check if the buildings are buildable and color them appropriately 309 for i, building in enumerate(self.buildings): 310 # get gfx for the building 311 # workaround for buildings like settler, that don't use the current level of 312 # the player, but always start at a certain lvl 313 level = self._class.get_initial_level(self.session.world.player) 314 315 if self._class.id == BUILDINGS.TREE and not building.buildable: 316 continue # Tree/ironmine that is not buildable, don't preview 317 else: 318 fife_instance, action_set_id = \ 319 self._class.getInstance(self.session, building.position.origin.x, 320 building.position.origin.y, rotation=building.rotation, 321 action=building.action, level=level, 322 action_set_id=self.buildings_action_set_ids[i]) 323 self.buildings_fife_instances[building] = fife_instance 324 # remember action sets per order of occurrence 325 # (this is far from good when building lines, but suffices for our purposes, which is mostly single build) 326 self.buildings_action_set_ids[i] = action_set_id 327 328 settlement = self.session.world.get_settlement(building.position.origin) 329 if settlement is not None and settlement.owner != self.session.world.player: 330 settlement = None # no fraternizing with the enemy, else there would be peace 331 332 if self._class.id != BUILDINGS.WAREHOUSE: 333 # Player shouldn't be allowed to build in this case, else it can trigger 334 # a new_settlement notification 335 if settlement is None: 336 building.buildable = False 337 338 # check required resources 339 (enough_res, missing_res) = Build.check_resources(needed_resources, self._class.costs, 340 self.session.world.player, [settlement, self.ship]) 341 if building.buildable and not enough_res: 342 # make building red 343 self.renderer.addColored(self.buildings_fife_instances[building], 344 *self.not_buildable_color) 345 building.buildable = False 346 # set missing info for gui 347 self.buildings_missing_resources[building] = missing_res 348 349 # color this instance with fancy stuff according to buildability 350 351 # this order determines highlight priority 352 # draw ordinary ranges first, then later color related buildings (they are more important) 353 self._make_surrounding_transparent(building) 354 self._color_preview_building(building) 355 if building.buildable: 356 self._draw_preview_building_range(building, settlement) 357 self._highlight_related_buildings_in_range(building, settlement) 358 self._highlight_inversely_related_buildings(building, settlement) 359 360 self.session.ingame_gui.resource_overview.set_construction_mode( 361 self.ship if self.ship is not None else settlement, 362 needed_resources 363 ) 364 self._add_listeners(self.ship if self.ship is not None else settlement)
365
366 - def _color_preview_building(self, building):
367 """Draw fancy stuff for build preview 368 @param building: return value from buildable, _BuildPosition 369 """ 370 if building.buildable: 371 # Tile might still have not buildable color -> remove it 372 self.renderer.removeColored(self.buildings_fife_instances[building]) 373 self.renderer.addOutlined(self.buildings_fife_instances[building], 374 self.buildable_color[0], self.buildable_color[1], 375 self.buildable_color[2], GFX.BUILDING_OUTLINE_WIDTH, 376 GFX.BUILDING_OUTLINE_THRESHOLD) 377 378 else: # not buildable 379 # must remove other highlight, fife does not support both 380 self.renderer.removeOutlined(self.buildings_fife_instances[building]) 381 self.renderer.addColored(self.buildings_fife_instances[building], 382 *self.not_buildable_color)
383
384 - def _draw_preview_building_range(self, building, settlement):
385 """Color the range as if the building was selected""" 386 radius_only_on_island = True 387 if hasattr(self.selectable_comp, 'range_applies_only_on_island'): 388 radius_only_on_island = self.selectable_comp.range_applies_only_on_island 389 390 self.selectable_comp.select_building(self.session, building.position, settlement, 391 self._class.radius, radius_only_on_island)
392 405
406 - def _make_surrounding_transparent(self, building):
407 """Makes the surrounding of building_position transparent and hide buildings 408 that are built upon (tearset)""" 409 world_contains = self.session.world.map_dimensions.contains_without_border 410 get_tile = self.session.world.get_tile 411 for coord in building.position.get_radius_coordinates(self.nearby_objects_radius, include_self=True): 412 p = Point(*coord) 413 if not world_contains(p): 414 continue 415 tile = get_tile(p) 416 if tile.object is not None and tile.object.buildable_upon: 417 inst = tile.object.fife_instance 418 inst.get2dGfxVisual().setTransparency(BUILDINGS.TRANSPARENCY_VALUE) 419 self._transparencified_instances.add(weakref.ref(inst)) 420 421 for to_tear_worldid in building.tearset: 422 inst = WorldObject.get_object_by_id(to_tear_worldid).fife_instance 423 inst.get2dGfxVisual().setTransparency(255) # full transparency = hidden 424 self._transparencified_instances.add(weakref.ref(inst))
425 452
454 """Inverse of highlight_related_buildings""" 455 # assemble list of all tiles that have are occupied by now restored buildings 456 # (if but one of them is in the range of something, the whole building 457 # might need to be colored as "in range") 458 modified_tiles = [] 459 for (building, was_selected) in self._highlighted_buildings: 460 inst = building.fife_instance 461 self.renderer.removeColored(inst) 462 # related buildings are highlighted, restore it 463 if was_selected: 464 self.renderer.addColored(inst, *self.selectable_comp.selection_color) 465 self.renderer.removeOutlined(inst) 466 modified_tiles.extend( 467 (self.session.world.get_tile(point) for point in building.position) 468 ) 469 self._highlighted_buildings.clear() 470 self.highlight_buildable(modified_tiles)
471
472 - def on_escape(self):
473 self._build_logic.on_escape(self.session) 474 if self.__class__.gui is not None: 475 self.__class__.gui.hide() 476 self.session.ingame_gui.set_cursor() # will call remove()
477
478 - def mouseMoved(self, evt):
479 self.log.debug("BuildingTool mouseMoved") 480 super().mouseMoved(evt) 481 point = self.get_world_location(evt) 482 if self.start_point != point: 483 self.start_point = point 484 self._check_update_preview(point) 485 evt.consume()
486
487 - def mousePressed(self, evt):
488 self.log.debug("BuildingTool mousePressed") 489 if evt.isConsumedByWidgets(): 490 super().mousePressed(evt) 491 return 492 if evt.getButton() == fife.MouseEvent.RIGHT: 493 self.on_escape() 494 elif evt.getButton() == fife.MouseEvent.LEFT: 495 pass 496 else: 497 super().mousePressed(evt) 498 return 499 evt.consume()
500
501 - def mouseDragged(self, evt):
502 self.log.debug("BuildingTool mouseDragged") 503 super().mouseDragged(evt) 504 point = self.get_world_location(evt) 505 if self.start_point is not None: 506 self._check_update_preview(point) 507 evt.consume()
508
509 - def mouseReleased(self, evt):
510 """Actually build.""" 511 self.log.debug("BuildingTool mouseReleased") 512 if evt.isConsumedByWidgets(): 513 super().mouseReleased(evt) 514 elif evt.getButton() == fife.MouseEvent.LEFT: 515 point = self.get_world_location(evt) 516 517 # check if position has changed with this event and update everything 518 self._check_update_preview(point) 519 520 # actually do the build 521 changed_tiles = self.do_build() 522 found_buildable = bool(changed_tiles) 523 if found_buildable: 524 PlaySound("build").execute(self.session, local=True) 525 526 # HACK: users sometimes don't realize that roads can be dragged 527 # check if 3 roads have been built within 1.2 seconds, and display 528 # a hint in case 529 if self._class.class_package == 'path': 530 import time 531 now = time.time() 532 BuildingTool._last_road_built.append(now) 533 if len(BuildingTool._last_road_built) > 2: 534 if (now - BuildingTool._last_road_built[-3]) < 1.2: 535 self.session.ingame_gui.message_widget.add('DRAG_ROADS_HINT') 536 # don't display hint multiple times at the same build situation 537 BuildingTool._last_road_built = [] 538 BuildingTool._last_road_built = BuildingTool._last_road_built[-3:] 539 540 # check how to continue: either build again or escape 541 shift = evt.isShiftPressed() or horizons.globals.fife.get_uh_setting('UninterruptedBuilding') 542 if ((shift and not self._class.id == BUILDINGS.WAREHOUSE) 543 or not found_buildable 544 or self._class.class_package == 'path'): 545 # build once more 546 self._restore_transparencified_instances() 547 self.highlight_buildable(changed_tiles) 548 self.start_point = point 549 self._build_logic.continue_build() 550 self.preview_build(point, point) 551 else: 552 self.on_escape() 553 evt.consume() 554 elif evt.getButton() != fife.MouseEvent.RIGHT: 555 super().mouseReleased(evt)
556
557 - def do_build(self):
558 """Actually builds the previews 559 @return a set of tiles where buildings have really been built""" 560 changed_tiles = set() 561 562 # actually do the build and build preparations 563 for i, building in enumerate(self.buildings): 564 # remove fife instance, the building will create a new one. 565 # Check if there is a matching fife instance, could be missing 566 # in case of trees, which are hidden if not buildable 567 if building in self.buildings_fife_instances: 568 fife_instance = self.buildings_fife_instances.pop(building) 569 self.renderer.removeColored(fife_instance) 570 self.renderer.removeOutlined(fife_instance) 571 fife_instance.getLocationRef().getLayer().deleteInstance(fife_instance) 572 573 if building.buildable: 574 island = self.session.world.get_island(building.position.origin) 575 for position in building.position: 576 tile = island.get_tile(position) 577 if tile in self._buildable_tiles: 578 # for some kind of buildabilities, not every coord of the 579 # building is buildable (e.g. fisher: only coastline is marked 580 # as buildable). For those tiles, that are not buildable, 581 # we don't need to do anything. 582 self._buildable_tiles.remove(tile) 583 self.renderer.removeColored(tile._instance) 584 changed_tiles.add(tile) 585 self._remove_listeners() # Remove changelisteners for update_preview 586 # create the command and execute it 587 cmd = Build(building=self._class, 588 x=building.position.origin.x, 589 y=building.position.origin.y, 590 rotation=building.rotation, 591 island=island, 592 settlement=self.session.world.get_settlement(building.position.origin), 593 ship=self.ship, 594 tearset=building.tearset, 595 action_set_id=self.buildings_action_set_ids[i], 596 ) 597 cmd.execute(self.session) 598 else: 599 if len(self.buildings) == 1: # only give messages for single bulds 600 # first, buildable reasons such as grounds 601 # second, resources 602 603 if building.problem is not None: 604 msg = building.problem[1] 605 self.session.ingame_gui.message_widget.add_custom( 606 point=building.position.origin, messagetext=msg) 607 608 # check whether to issue a missing res notification 609 # we need the localized resource name here 610 elif building in self.buildings_missing_resources: 611 res_name = self.session.db.get_res_name(self.buildings_missing_resources[building]) 612 self.session.ingame_gui.message_widget.add( 613 point=building.position.origin, 614 string_id='NEED_MORE_RES', message_dict={'resource': res_name}) 615 616 self.buildings = [] 617 self.buildings_action_set_ids = [] 618 return changed_tiles
619
620 - def _check_update_preview(self, end_point):
621 """Used internally if the end_point changes""" 622 if self.end_point != end_point: 623 self.end_point = end_point 624 self.update_preview()
625
626 - def _remove_listeners(self):
627 """Resets the ChangeListener for update_preview.""" 628 if self.last_change_listener is not None: 629 self.last_change_listener.discard_change_listener(self.force_update) 630 self.last_change_listener.discard_change_listener(self.highlight_buildable) 631 self._build_logic.remove_change_listener(self.last_change_listener, self) 632 633 self.last_change_listener = None
634
635 - def _add_listeners(self, instance):
636 if self.last_change_listener != instance: 637 self._remove_listeners() 638 self.last_change_listener = instance 639 if self.last_change_listener is not None: 640 self._build_logic.add_change_listener(self.last_change_listener, self)
641
642 - def force_update(self):
643 self.update_preview(force=True)
644
645 - def update_preview(self, force=False):
646 """Used as callback method""" 647 if self.start_point is not None: 648 end_point = self.end_point or self.start_point 649 self.preview_build(self.start_point, end_point, force=force)
650
651 - def _rotate(self, degrees):
652 self.rotation = (self.rotation + degrees) % 360 653 self.log.debug("BuildingTool: Building rotation now: %s", self.rotation) 654 self.update_preview() 655 self.draw_gui()
656
657 - def rotate_left(self):
658 self._rotate(degrees=90)
659
660 - def rotate_right(self):
661 self._rotate(degrees=270)
662
664 """Deletes fife instances of buildings""" 665 666 try: 667 self._class.get_component_template(SelectableComponent) 668 except KeyError: 669 pass 670 else: 671 deselected_tiles = self.selectable_comp.deselect_building(self.session) 672 # redraw buildables (removal of selection might have tampered with it) 673 self.highlight_buildable(deselected_tiles) 674 675 self._restore_transparencified_instances() 676 self._restore_highlighted_buildings() 677 for building in self._related_buildings: 678 # restore selection, removeOutline can destroy it 679 building.get_component(SelectableComponent).set_selection_outline() 680 for fife_instance in self.buildings_fife_instances.values(): 681 layer = fife_instance.getLocationRef().getLayer() 682 # layer might not exist, happens for some reason after a build 683 if layer is not None: 684 layer.deleteInstance(fife_instance) 685 self.buildings_fife_instances = {}
686
688 """Removes transparency""" 689 for inst_weakref in self._transparencified_instances: 690 fife_instance = inst_weakref() 691 if fife_instance: 692 # remove transparency only if trees aren't supposed to be transparent as default 693 if not hasattr(fife_instance, "keep_translucency") or not fife_instance.keep_translucency: 694 fife_instance.get2dGfxVisual().setTransparency(0) 695 else: 696 # restore regular translucency value, can also be different 697 fife_instance.get2dGfxVisual().setTransparency(BUILDINGS.TRANSPARENCY_VALUE) 698 self._transparencified_instances.clear()
699
700 - def _remove_coloring(self):
701 """Removes coloring from tiles, that indicate that the tile is buildable 702 as well as all highlights. Called when building mode is finished.""" 703 for building in self._related_buildings: 704 building.get_component(SelectableComponent).deselect() 705 self.renderer.removeAllOutlines() 706 self.renderer.removeAllColored()
707 708
709 -class ShipBuildingToolLogic:
710 """Helper class to separate the logic needed when building from a ship from 711 the main building tool.""" 712
713 - def __init__(self, ship):
714 self.ship = ship
715
716 - def highlight_buildable(self, building_tool, tiles_to_check=None):
717 """Highlights all buildable tiles. 718 @param tiles_to_check: list of tiles to check for coloring.""" 719 # resolved variables from inner loops 720 is_tile_buildable = building_tool._class.is_tile_buildable 721 session = building_tool.session 722 player = session.world.player 723 buildable_tiles_add = building_tool._buildable_tiles.add 724 725 if tiles_to_check is not None: # only check these tiles (build from ship) 726 for tile in tiles_to_check: 727 if is_tile_buildable(session, tile, self.ship): 728 building_tool._color_buildable_tile(tile) 729 else: # build from ship 730 building_tool.renderer.removeAllColored() 731 for island in session.world.get_islands_in_radius(self.ship.position, self.ship.radius): 732 for tile in island.get_surrounding_tiles(self.ship.position, self.ship.radius): 733 if is_tile_buildable(session, tile, self.ship): 734 buildable_tiles_add(tile) 735 # check that there is no other player's settlement 736 if tile.settlement is None or tile.settlement.owner == player: 737 building_tool._color_buildable_tile(tile)
738
739 - def on_escape(self, session):
740 for selected in session.selected_instances: 741 selected.get_component(SelectableComponent).deselect() 742 session.selected_instances = set([self.ship]) 743 self.ship.get_component(SelectableComponent).select() 744 self.ship.get_component(SelectableComponent).show_menu()
745
746 - def remove(self, session):
747 self.on_escape(session)
748
749 - def add_change_listener(self, instance, building_tool):
750 # instance is self.ship here 751 instance.add_change_listener(building_tool.highlight_buildable) 752 instance.add_change_listener(building_tool.force_update)
753
754 - def remove_change_listener(self, instance, building_tool):
755 # be idempotent 756 instance.discard_change_listener(building_tool.highlight_buildable) 757 instance.discard_change_listener(building_tool.force_update)
758 759 # Using messages now.
760 - def continue_build(self):
761 pass
762 763
764 -class SettlementBuildingToolLogic:
765 """Helper class to separate the logic needed when building from a settlement 766 from the main building tool""" 767
768 - def __init__(self, building_tool):
769 self.building_tool = weakref.ref(building_tool) 770 self.subscribed = False
771
772 - def highlight_buildable(self, building_tool, tiles_to_check=None):
773 """Highlights all buildable tiles. 774 @param tiles_to_check: list of tiles to check for coloring.""" 775 776 # resolved variables from inner loops 777 is_tile_buildable = building_tool._class.is_tile_buildable 778 session = building_tool.session 779 player = session.world.player 780 781 if not self.subscribed: 782 self.subscribed = True 783 SettlementRangeChanged.subscribe(self._on_update) 784 785 if tiles_to_check is not None: 786 # Only check these tiles. 787 for tile in tiles_to_check: 788 if is_tile_buildable(session, tile, None): 789 building_tool._color_buildable_tile(tile) 790 else: 791 # Default build on island. 792 for settlement in session.world.settlements: 793 if settlement.owner == player: 794 island = session.world.get_island(Point(*next(iter(settlement.ground_map.keys())))) 795 for tile in settlement.ground_map.values(): 796 if is_tile_buildable(session, tile, None, island, check_settlement=False): 797 building_tool._color_buildable_tile(tile)
798
799 - def _on_update(self, message):
800 if self.building_tool() and message.sender.owner.is_local_player: 801 # this is generally caused by adding new buildings, therefore new_buildings=True 802 self.building_tool().highlight_buildable(message.changed_tiles, new_buildings=True)
803
804 - def on_escape(self, session):
805 session.ingame_gui.show_build_menu() # This will call remove(). 806 if self.subscribed: 807 self.subscribed = False 808 SettlementRangeChanged.unsubscribe(self._on_update)
809
810 - def remove(self, session):
811 if self.subscribed: 812 self.subscribed = False 813 SettlementRangeChanged.unsubscribe(self._on_update)
814 815 # Using messages now.
816 - def add_change_listener(self, instance, building_tool):
817 pass
818
819 - def remove_change_listener(self, instance, building_tool):
820 pass
821
822 - def continue_build(self):
823 pass
824 825
826 -class BuildRelatedBuildingToolLogic(SettlementBuildingToolLogic):
827 """Same as normal build, except quitting it drops to the build related tab."""
828 - def __init__(self, building_tool, instance):
829 super().__init__(building_tool) 830 # instance must be weakref 831 self.instance = instance
832
833 - def _reshow_tab(self):
834 from horizons.gui.tabs import BuildRelatedTab 835 self.instance().get_component(SelectableComponent).show_menu(jump_to_tabclass=BuildRelatedTab)
836
837 - def on_escape(self, session):
838 super().on_escape(session) 839 self._reshow_tab()
840
841 - def continue_build(self):
842 self._reshow_tab()
843