Package horizons :: Package component :: Module selectablecomponent
[hide private]
[frames] | no frames]

Source Code for Module horizons.component.selectablecomponent

  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 copy 
 23  import itertools 
 24  import operator 
 25   
 26  from fife import fife 
 27   
 28  import horizons.globals 
 29  from horizons.component import Component 
 30  from horizons.constants import GFX, LAYERS, RES 
 31  from horizons.util.shapes import RadiusRect 
32 33 34 -class SelectableComponent(Component):
35 """Stuff you can select. 36 Has to be subdivided in buildings and units, which is further specialized to ships. 37 38 Provides: 39 show_menu(): shows tabs 40 select(): highlight instance visually 41 deselect(): inverse of select 42 43 show_menu() and select() are frequently used in combination. 44 45 The definitions must contain type, tabs and enemy_tabs. 46 """ 47 48 NAME = "selectablecomponent" 49 50 @classmethod
51 - def get_instance(cls, arguments):
52 # this can't be class variable because the classes aren't defined when 53 # it would be parsed 54 TYPES = {'building': SelectableBuildingComponent, 55 'unit': SelectableUnitComponent, 56 'ship': SelectableShipComponent, 57 'fisher': SelectableFisherComponent, } 58 arguments = copy.copy(arguments) 59 t = arguments.pop('type') 60 return TYPES[t](**arguments)
61
62 - def __init__(self, tabs, enemy_tabs, active_tab=None):
63 super().__init__() 64 # resolve tab 65 from horizons.gui.tabs import resolve_tab 66 self.tabs = list(map(resolve_tab, tabs)) 67 self.enemy_tabs = list(map(resolve_tab, enemy_tabs)) 68 self.active_tab = resolve_tab(active_tab) if active_tab is not None else None 69 self._selected = False
70
71 - def show_menu(self, jump_to_tabclass=None):
72 """Shows tabwidget tabs of this instance. 73 74 Opens the first such tab unless jump_to_tabclass specifies otherwise. 75 @param jump_to_tabclass: open the first tab that is a subclass to this parameter 76 """ 77 from horizons.gui.tabs import TabWidget 78 tablist = None 79 if self.instance.owner is not None and self.instance.owner.is_local_player: 80 tablist = self.tabs 81 else: # this is an enemy instance with respect to the local player 82 tablist = self.enemy_tabs 83 84 if not tablist: 85 return 86 87 tabclasses = [tabclass for tabclass in tablist if tabclass.shown_for(self.instance)] 88 try: 89 active_tab_index = tabclasses.index(self.active_tab) 90 except ValueError: 91 active_tab_index = None 92 tabs = [tabclass(self.instance) for tabclass in tabclasses] 93 tabwidget = TabWidget(self.session.ingame_gui, tabs=tabs, active_tab=active_tab_index) 94 95 if jump_to_tabclass: 96 for i, tab in enumerate(tabs): 97 if isinstance(tab, jump_to_tabclass): 98 tabwidget.show_tab(i) 99 break 100 self.session.ingame_gui.show_menu(tabwidget)
101
102 - def select(self, reset_cam=False):
103 self._selected = True 104 if reset_cam: 105 self.session.view.center(*self.instance.position.center.to_tuple())
106
107 - def deselect(self):
108 self._selected = False
109 110 @property
111 - def selected(self):
112 return self._selected
113
114 - def remove(self):
115 if self.instance in self.session.selected_instances: 116 self.session.selected_instances.remove(self.instance) 117 for group in self.session.selection_groups: 118 group.discard(self) 119 if self._selected: 120 self.deselect() 121 super().remove()
122
123 124 -class SelectableBuildingComponent(SelectableComponent):
125 126 selection_color = (255, 255, 32, 192) 127 128 # these smell like instance attributes, but sometimes have to be used in non-instance 129 # contexts (e.g. building tool).
130 - class ListHolder:
131 - def __init__(self):
132 self.l = []
133 134 # read/write on class variables is somewhat borked in python, so 135 _selected_tiles = ListHolder() # tiles that are selected. used for clean deselect. 136 _selected_fake_tiles = ListHolder() # fake tiles create over ocean to select (can't select ocean directly) 137 138 @classmethod
139 - def reset(cls):
140 """Called on session end to get rid of static data and init variables""" 141 cls._selected_tiles.l = [] 142 cls._selected_fake_tiles.l = []
143
144 - def __init__(self, tabs, enemy_tabs, active_tab=None, range_applies_only_on_island=True):
145 super().__init__(tabs, enemy_tabs, active_tab=active_tab) 146 self.range_applies_only_on_island = range_applies_only_on_island
147
148 - def initialize(self):
149 # check for related buildings (defined in db, not yaml) 150 related_buildings = self.session.db.get_related_building_ids_for_menu(self.instance.id) 151 if related_buildings: 152 from horizons.gui.tabs import BuildRelatedTab 153 self.tabs += (BuildRelatedTab,)
154
155 - def load(self, db, worldid):
156 self.initialize()
157
158 - def select(self, reset_cam=False):
159 """Runs necessary steps to select the building.""" 160 super().select(reset_cam) 161 self.set_selection_outline() 162 if self.instance.owner is None or not self.instance.owner.is_local_player: 163 return # don't show enemy ranges 164 renderer = self.session.view.renderer['InstanceRenderer'] 165 self._do_select(renderer, self.instance.position, self.session.world, 166 self.instance.settlement, self.instance.radius, self.range_applies_only_on_island)
167
168 - def set_selection_outline(self):
169 """Only set the selection outline. 170 Useful when it has been removed by some kind of interference""" 171 renderer = self.session.view.renderer['InstanceRenderer'] 172 renderer.addOutlined(self.instance._instance, self.selection_color[0], self.selection_color[1], 173 self.selection_color[2], GFX.BUILDING_OUTLINE_WIDTH, 174 GFX.BUILDING_OUTLINE_THRESHOLD)
175
176 - def deselect(self):
177 """Runs neccassary steps to deselect the building. 178 Only deselects if this building has been selected.""" 179 if self._selected: 180 super().deselect() 181 renderer = self.session.view.renderer['InstanceRenderer'] 182 renderer.removeOutlined(self.instance._instance) 183 renderer.removeAllColored() 184 for fake_tile in self.__class__._selected_fake_tiles.l: 185 self.session.view.layers[LAYERS.FIELDS].deleteInstance(fake_tile) 186 self.__class__._selected_fake_tiles.l = []
187 188 @classmethod
189 - def select_building(cls, session, position, settlement, 190 radius, range_applies_only_on_island):
191 """Select a hypothecial instance of this class. Use Case: Buildingtool. 192 Only works on a subclass of BuildingClass, since it requires certain class attributes. 193 @param session: Session instance 194 @param position: Position of building, usually Rect 195 @param settlement: Settlement instance the building belongs to""" 196 renderer = session.view.renderer['InstanceRenderer'] 197 198 cls._do_select(renderer, position, session.world, settlement, 199 radius, range_applies_only_on_island)
200 201 @classmethod
202 - def deselect_building(cls, session):
203 """@see select_building 204 Used by building tool, allows incremental updates 205 @return list of tiles that were deselected (only normal tiles, no fake tiles)""" 206 remove_colored = session.view.renderer['InstanceRenderer'].removeColored 207 for tile in cls._selected_tiles.l: 208 remove_colored(tile._instance) 209 if tile.object is not None: 210 remove_colored(tile.object._instance) 211 selected_tiles = cls._selected_tiles.l 212 cls._selected_tiles.l = [] 213 for fake_tile in cls._selected_fake_tiles.l: 214 session.view.layers[LAYERS.FIELDS].deleteInstance(fake_tile) 215 cls._selected_fake_tiles.l = [] 216 return selected_tiles
217 218 @classmethod
219 - def select_many(cls, buildings, renderer):
220 """Same as calling select() on many instances, but way faster. 221 Limited functionality, only use on real buildings of a settlement.""" 222 if not buildings: 223 return [] # that is not many 224 225 selected_tiles = [] 226 227 # group buildings per settlement and treat them separately 228 # they cannot share tiles, and we can then just access the settlements ground map 229 buildings_sorted = sorted(buildings, key=operator.attrgetter('settlement')) 230 for settlement, buildings in itertools.groupby( 231 buildings_sorted, operator.attrgetter('settlement')): 232 # resolve operator 233 buildings = list(buildings) 234 235 for building in buildings: 236 building.get_component(SelectableComponent).set_selection_outline() 237 238 coords = {coord for 239 building in buildings for 240 coord in building.position.get_radius_coordinates(building.radius, include_self=True)} 241 242 for coord in coords: 243 tile = settlement.ground_map.get(coord) 244 if tile: 245 if ('constructible' in tile.classes or 'coastline' in tile.classes): 246 cls._add_selected_tile(tile, renderer) 247 selected_tiles.append(tile) 248 return selected_tiles
249 250 @classmethod
251 - def _do_select(cls, renderer, position, world, settlement, 252 radius, range_applies_only_on_island):
253 island = world.get_island(position.origin) 254 if island is None: 255 return # preview isn't on island, and therefore invalid 256 257 if range_applies_only_on_island: 258 ground_holder = None # use settlement or island as tile provider (prefer settlement, since it contains fewer tiles) 259 if settlement is None: 260 ground_holder = island 261 else: 262 ground_holder = settlement 263 264 for tile in ground_holder.get_tiles_in_radius(position, radius, include_self=False): 265 if 'constructible' in tile.classes or 'coastline' in tile.classes: 266 if settlement is None and tile.settlement is not None: 267 # trying to build a warehouse and the tile is already owned by another player. 268 continue 269 cls._add_selected_tile(tile, renderer) 270 else: 271 # we have to color water too 272 # since water tiles are huge, create fake tiles and color them 273 cls._init_fake_tile() 274 275 layer = world.session.view.layers[LAYERS.FIELDS] 276 # color island or fake tile 277 for tup in position.get_radius_coordinates(radius): 278 tile = island.get_tile_tuple(tup) 279 if tile is not None: 280 cls._add_selected_tile(tile, renderer) 281 else: # need extra tile 282 cls._add_fake_tile(tup[0], tup[1], layer, renderer)
283 284 @classmethod
285 - def _init_fake_tile(cls):
286 """Sets the _fake_tile_obj class variable with a ready to use fife object. 287 288 To create a new fake tile, use _add_fake_tile().""" 289 # use fixed SelectableBuildingComponent here, to make sure subclasses also read the same variable 290 if not hasattr(SelectableBuildingComponent, "_fake_tile_obj"): 291 # create object to create instances from 292 fake_tile_obj = horizons.globals.fife.engine.getModel().createObject('fake_tile_obj', 'ground') 293 SelectableBuildingComponent._fake_tile_obj = fake_tile_obj 294 fife.ObjectVisual.create(SelectableBuildingComponent._fake_tile_obj) 295 296 img_path = 'content/gfx/fake_water.png' 297 img = horizons.globals.fife.imagemanager.load(img_path) 298 for rotation in [45, 135, 225, 315]: 299 SelectableBuildingComponent._fake_tile_obj.get2dGfxVisual().addStaticImage(rotation, img.getHandle())
300 301 @classmethod
302 - def _add_fake_tile(cls, x, y, layer, renderer):
303 """Adds a fake tile to the position. Requires 'cls._fake_tile_obj' to be set.""" 304 inst = layer.createInstance(SelectableBuildingComponent._fake_tile_obj, 305 fife.ModelCoordinate(x, y, 0), "") 306 fife.InstanceVisual.create(inst) 307 cls._selected_fake_tiles.l.append(inst) 308 renderer.addColored(inst, *cls.selection_color)
309 310 @classmethod
311 - def _add_selected_tile(cls, tile, renderer, remember=True):
312 """ 313 @param remember: whether to keep track of this tile. Set to False on recolorings. 314 """ 315 if remember: 316 cls._selected_tiles.l.append(tile) 317 renderer.addColored(tile._instance, *cls.selection_color) 318 # Add color to objects on the tiles 319 obj = tile.object 320 if obj is not None: 321 renderer.addColored(obj._instance, *cls.selection_color)
322
323 324 -class SelectableUnitComponent(SelectableComponent):
325
326 - def select(self, reset_cam=False):
327 """Runs necessary steps to select the unit.""" 328 super().select(reset_cam) 329 self.session.view.renderer['InstanceRenderer'].addOutlined(self.instance._instance, 255, 255, 255, GFX.UNIT_OUTLINE_WIDTH, GFX.UNIT_OUTLINE_THRESHOLD) 330 self.instance.draw_health() 331 self.session.view.add_change_listener(self.instance.draw_health)
332
333 - def deselect(self):
334 """Runs necessary steps to deselect the unit.""" 335 if not self._selected: 336 return 337 super().deselect() 338 self.session.view.renderer['InstanceRenderer'].removeOutlined(self.instance._instance) 339 self.instance.draw_health(remove_only=True) 340 # this is necessary to make deselect idempotent 341 self.session.view.discard_change_listener(self.instance.draw_health)
342
343 344 -class SelectableShipComponent(SelectableUnitComponent):
345
346 - def select(self, reset_cam=False):
347 """Runs necessary steps to select the ship.""" 348 super().select(reset_cam=reset_cam) 349 350 # add a buoy at the ship's target if the player owns the ship 351 if self.instance.owner.is_local_player: 352 self.instance._update_buoy() 353 self.session.ingame_gui.minimap.show_unit_path(self.instance)
354
355 - def deselect(self):
356 """Runs necessary steps to deselect the ship.""" 357 if self._selected: 358 super().deselect() 359 self.instance._update_buoy(remove_only=True)
360
361 362 -class SelectableFisherComponent(SelectableBuildingComponent):
363 """Class used to highlight the radius of a fisher. Highlights only the fishing 364 grounds.""" 365 366 @classmethod
367 - def _do_select(cls, renderer, position, world, settlement, radius, 368 range_applies_only_on_island):
369 # No super, we don't want to color the ground 370 cls._init_fake_tile() 371 layer = world.session.view.layers[LAYERS.FIELDS] 372 for fish_deposit in world.get_providers_in_range(RadiusRect(position, radius), res=RES.FISH): 373 #renderer.addColored(fish_deposit._instance, *cls.selection_color) 374 #cls._selected_tiles.l.append(fish_deposit) 375 for pos in fish_deposit.position: 376 cls._add_fake_tile(pos.x, pos.y, layer, renderer)
377