Package horizons :: Package world :: Package building :: Module settler
[hide private]
[frames] | no frames]

Source Code for Module horizons.world.building.settler

  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 math 
 24   
 25  from horizons.command.building import Build 
 26  from horizons.command.production import ToggleActive 
 27  from horizons.component.collectingcomponent import CollectingComponent 
 28  from horizons.component.storagecomponent import StorageComponent 
 29  from horizons.constants import BUILDINGS, GAME, RES, TIER 
 30  from horizons.gui.tabs import SettlerOverviewTab 
 31  from horizons.messaging import ( 
 32          AddStatusIcon, RemoveStatusIcon, SettlerInhabitantsChanged, SettlerUpdate, 
 33          UpgradePermissionsChanged) 
 34  from horizons.scheduler import Scheduler 
 35  from horizons.util.pathfinding.pather import StaticPather 
 36  from horizons.util.python.callback import Callback 
 37  from horizons.world.building.buildable import BuildableRect, BuildableSingle 
 38  from horizons.world.building.building import BasicBuilding 
 39  from horizons.world.building.buildingresourcehandler import BuildingResourceHandler 
 40  from horizons.world.production.producer import Producer 
 41  from horizons.world.production.production import SettlerProduction 
 42  from horizons.world.status import SettlerNotConnectedStatus, SettlerUnhappyStatus 
43 44 45 -class SettlerRuin(BasicBuilding, BuildableSingle):
46 """Building that appears when a settler got unhappy. The building does nothing. 47 48 NOTE: Inheriting from BuildableSingle is necessary, cause it's built via Build Command, which 49 checks for buildability 50 """ 51 buildable_upon = True 52 walkable = True
53
54 55 -class Settler(BuildableRect, BuildingResourceHandler, BasicBuilding):
56 """Represents a settlers house, that uses resources and creates inhabitants.""" 57 log = logging.getLogger("world.building.settler") 58 59 production_class = SettlerProduction 60 61 tabs = (SettlerOverviewTab, ) 62 63 default_level_on_build = 0 64
65 - def __init__(self, x, y, owner, instance=None, **kwargs):
66 kwargs['level'] = self.__class__.default_level_on_build # settlers always start in first level 67 super().__init__(x=x, y=y, owner=owner, instance=instance, **kwargs)
68
69 - def __init(self, loading=False, last_tax_payed=0):
70 self.level_max = TIER.CURRENT_MAX # for now 71 self._update_level_data(loading=loading, initial=True) 72 self.last_tax_payed = last_tax_payed 73 UpgradePermissionsChanged.subscribe(self._on_change_upgrade_permissions, sender=self.settlement) 74 self._upgrade_production = None # referenced here for quick access
75
76 - def initialize(self):
77 super().initialize() 78 SettlerInhabitantsChanged.broadcast(self, self.inhabitants) 79 happiness = self.__get_data("happiness_init_value") 80 if happiness is not None: 81 self.get_component(StorageComponent).inventory.alter(RES.HAPPINESS, happiness) 82 if self.has_status_icon: 83 self.get_component(StorageComponent).inventory.add_change_listener(self._update_status_icon) 84 # give the user a month (about 30 seconds) to build a main square in range 85 if self.owner.is_local_player: 86 Scheduler().add_new_object(self._check_main_square_in_range, self, Scheduler().get_ticks_of_month(), loops=-1) 87 self.__init() 88 self.run()
89
90 - def save(self, db):
91 super().save(db) 92 db("INSERT INTO settler(rowid, inhabitants, last_tax_payed) VALUES (?, ?, ?)", 93 self.worldid, self.inhabitants, self.last_tax_payed) 94 remaining_ticks = Scheduler().get_remaining_ticks(self, self._tick) 95 db("INSERT INTO remaining_ticks_of_month(rowid, ticks) VALUES (?, ?)", 96 self.worldid, remaining_ticks)
97
98 - def load(self, db, worldid):
99 super().load(db, worldid) 100 self.inhabitants, last_tax_payed = \ 101 db("SELECT inhabitants, last_tax_payed FROM settler WHERE rowid=?", worldid)[0] 102 remaining_ticks = \ 103 db("SELECT ticks FROM remaining_ticks_of_month WHERE rowid=?", worldid)[0][0] 104 self.__init(loading=True, last_tax_payed=last_tax_payed) 105 self._load_upgrade_data(db) 106 SettlerUpdate.broadcast(self, self.level, self.level) 107 self.run(remaining_ticks)
108
109 - def _load_upgrade_data(self, db):
110 """Load the upgrade production and relevant stored resources""" 111 upgrade_material_prodline = SettlerUpgradeData.get_production_line_id(self.level + 1) 112 if not self.get_component(Producer).has_production_line(upgrade_material_prodline): 113 return 114 115 self._upgrade_production = self.get_component(Producer)._get_production(upgrade_material_prodline) 116 117 # readd the res we already had, they can't be loaded since storage slot limits for 118 # the special resources aren't saved 119 resources = {} 120 for resource, amount in db.get_storage_rowids_by_ownerid(self.worldid): 121 resources[resource] = amount 122 123 for res, amount in self._upgrade_production.get_consumed_resources().items(): 124 # set limits to what we need 125 self.get_component(StorageComponent).inventory.add_resource_slot(res, abs(amount)) 126 if res in resources: 127 self.get_component(StorageComponent).inventory.alter(res, resources[res]) 128 129 self._upgrade_production.add_production_finished_listener(self.level_up) 130 self.log.debug("%s: Waiting for material to upgrade from %s", self, self.level)
131
133 """ 134 Add a production line that gets the necessary upgrade material. 135 When the production finishes, it calls upgrade_materials_collected. 136 """ 137 upgrade_material_prodline = SettlerUpgradeData.get_production_line_id(self.level + 1) 138 self._upgrade_production = self.get_component( 139 Producer).add_production_by_id(upgrade_material_prodline) 140 self._upgrade_production.add_production_finished_listener(self.level_up) 141 142 # drive the car out of the garage to make space for the building material 143 for res, amount in self._upgrade_production.get_consumed_resources().items(): 144 self.get_component(StorageComponent).inventory.add_resource_slot(res, abs(amount)) 145 146 self.log.debug("%s: Waiting for material to upgrade from %s", self, self.level)
147
148 - def remove(self):
153 154 @property
155 - def upgrade_allowed(self):
156 return self.session.world.get_settlement(self.position.origin).upgrade_permissions[self.level]
157
158 - def _on_change_upgrade_permissions(self, message):
159 production = self._upgrade_production 160 if production is not None: 161 if production.is_paused() == self.upgrade_allowed: 162 ToggleActive(self.get_component(Producer), production).execute(self.session, True)
163 164 @property
165 - def happiness(self):
166 difficulty = self.owner.difficulty 167 result = int(round(difficulty.extra_happiness_constant + self.get_component(StorageComponent).inventory[RES.HAPPINESS] * difficulty.happiness_multiplier)) 168 return max(0, min(result, self.get_component(StorageComponent).inventory.get_limit(RES.HAPPINESS)))
169 170 @property
171 - def capacity_utilization(self):
172 # this concept does not make sense here, so spare us the calculations 173 return 1.0
174
175 - def _update_level_data(self, loading=False, initial=False):
176 """Updates all settler-related data because of a level change or as initialization 177 @param loading: whether called to set data after loading 178 @param initial: whether called to set data initially 179 """ 180 # taxes, inhabitants 181 self.tax_base = self.session.db.get_settler_tax_income(self.level) 182 self.inhabitants_max = self.session.db.get_tier_inhabitants_max(self.level) 183 self.inhabitants_min = self.session.db.get_tier_inhabitants_min(self.level) 184 #TODO This crops inhabitants at level down, but when can they exceed the limit? 185 if self.inhabitants > self.inhabitants_max: 186 self.inhabitants = self.inhabitants_max 187 188 # consumption: 189 # Settler productions are specified to be disabled by default in the db, so we can enable 190 # them here per level. Production data is save/loaded, so we don't need to do anything in that case 191 if not loading: 192 prod_comp = self.get_component(Producer) 193 current_lines = prod_comp.get_production_lines() 194 for prod_line in prod_comp.get_production_lines_by_level(self.level): 195 if not prod_comp.has_production_line(prod_line): 196 prod_comp.add_production_by_id(prod_line) 197 # cross out the new lines from the current lines, so only the old ones remain 198 if prod_line in current_lines: 199 current_lines.remove(prod_line) 200 for line in current_lines[:]: # iterate over copy for safe removal 201 # all lines, that were added here but are not used due to the current level 202 # NOTE: this contains the upgrade material production line 203 prod_comp.remove_production_by_id(line) 204 # Make sure to set _upgrade_production to None in case we are removing it 205 if self._upgrade_production is not None and line == self._upgrade_production.get_production_line_id(): 206 self._upgrade_production = None 207 208 if not initial: 209 # update instance graphics 210 # only do it when something has actually change 211 212 # TODO: this probably also isn't necessary on loading, but it's 213 # not touched before the relase (2012.1) 214 self.update_action_set_level(self.level)
215
216 - def run(self, remaining_ticks=None):
217 """Start regular tick calls""" 218 interval = self.session.timer.get_ticks(GAME.INGAME_TICK_INTERVAL) 219 run_in = remaining_ticks if remaining_ticks is not None else interval 220 Scheduler().add_new_object(self._tick, self, run_in=run_in, loops=-1, loop_interval=interval)
221
222 - def _tick(self):
223 """Here we collect the functions, that are called regularly (every "month").""" 224 self.pay_tax() 225 self.inhabitant_check() 226 self.level_check()
227
228 - def pay_tax(self):
229 """Pays the tax for this settler""" 230 # the money comes from nowhere, settlers seem to have an infinite amount of money. 231 # see https://github.com/unknown-horizons/unknown-horizons/wiki/Settler-taxing 232 233 # calc taxes https://github.com/unknown-horizons/unknown-horizons/wiki/Settler-taxing#Formulae 234 happiness_tax_modifier = 0.5 + (float(self.happiness) / 70.0) 235 inhabitants_tax_modifier = float(self.inhabitants) / self.inhabitants_max 236 taxes = self.tax_base * self.settlement.tax_settings[self.level] * happiness_tax_modifier * inhabitants_tax_modifier 237 real_taxes = int(round(taxes * self.owner.difficulty.tax_multiplier)) 238 239 self.settlement.owner.get_component(StorageComponent).inventory.alter(RES.GOLD, real_taxes) 240 self.last_tax_payed = real_taxes 241 242 # decrease happiness https://github.com/unknown-horizons/unknown-horizons/wiki/Settler-taxing#Formulae 243 difference = 1.0 - self.settlement.tax_settings[self.level] 244 happiness_decrease = 10 * difference - 6 * abs(difference) 245 happiness_decrease = int(round(happiness_decrease)) 246 # NOTE: this formula was actually designed for a different use case, where the happiness 247 # is calculated from the number of available goods -/+ a certain tax factor. 248 # to simulate the more dynamic, currently implemented approach (where every event changes 249 # the happiness), we simulate discontent of taxes by this: 250 happiness_decrease -= 6 251 self.get_component(StorageComponent).inventory.alter(RES.HAPPINESS, happiness_decrease) 252 self._changed() 253 self.log.debug("%s: pays %s taxes, -happy: %s new happiness: %s", self, real_taxes, 254 happiness_decrease, self.happiness)
255
256 - def inhabitant_check(self):
257 """Checks whether or not the population of this settler should increase or decrease""" 258 sad = self.session.db.get_lower_happiness_limit() 259 happy = self.session.db.get_upper_happiness_limit() 260 change = 0 261 if self.happiness > happy and self.inhabitants < self.inhabitants_max: 262 change = 1 263 self.log.debug("%s: inhabitants increase to %s", self, self.inhabitants) 264 elif self.happiness < sad and self.inhabitants > 1: 265 change = -1 266 self.log.debug("%s: inhabitants decrease to %s", self, self.inhabitants) 267 268 if change != 0: 269 # see https://github.com/unknown-horizons/unknown-horizons/wiki/Supply-citizens-with-resources 270 self.get_component(Producer).alter_production_time( 271 6.0 / 7.0 * math.log(1.5 * (self.inhabitants + 1.2))) 272 self.inhabitants += change 273 SettlerInhabitantsChanged.broadcast(self, change) 274 self._changed()
275
276 - def can_level_up(self):
277 return self.happiness > self.__get_data("happiness_level_up_requirement") and \ 278 self.inhabitants >= self.inhabitants_min and not self._has_disaster()
279
280 - def level_check(self):
281 """Checks whether we should level up or down. 282 283 Ignores buildings with a active disaster. """ 284 if self.can_level_up(): 285 if self.level >= self.level_max: 286 # max level reached already, can't allow an update 287 if self.owner.max_tier_notification < self.level_max: 288 if self.owner.is_local_player: 289 self.session.ingame_gui.message_widget.add( 290 point=self.position.center, string_id='MAX_TIER_REACHED') 291 self.owner.max_tier_notification = self.level_max 292 return 293 if self._upgrade_production: 294 return # already waiting for res 295 296 self._add_upgrade_production_line() 297 298 if not self.upgrade_allowed: 299 ToggleActive(self.get_component(Producer), self._upgrade_production).execute(self.session, True) 300 301 elif self.happiness < self.__get_data("happiness_level_down_limit") or \ 302 self.inhabitants < self.inhabitants_min: 303 self.level_down() 304 self._changed()
305
306 - def level_up(self, production=None):
307 """Actually level up (usually called when the upgrade material has arrived)""" 308 309 self._upgrade_production = None 310 311 # just level up later that tick, it could disturb other code higher in the call stack 312 313 def _do_level_up(): 314 self.level += 1 315 self.log.debug("%s: Levelling up to %s", self, self.level) 316 self._update_level_data() 317 318 # update the level of our inhabitants so graphics can change 319 if self.has_component(CollectingComponent): 320 for collector in self.get_component(CollectingComponent).get_local_collectors(): 321 collector.level_upgrade(self.level) 322 323 # Notify the world about the level up 324 SettlerUpdate.broadcast(self, self.level, 1) 325 326 # reset happiness value for new level 327 new_happiness = self.__get_data("happiness_init_value") - self.happiness 328 self.get_component(StorageComponent).inventory.alter(RES.HAPPINESS, new_happiness) 329 self._changed()
330 331 Scheduler().add_new_object(_do_level_up, self, run_in=0)
332
333 - def level_down(self):
334 if self.level == TIER.LOWEST: 335 # Can't level down any more. 336 self.make_ruin() 337 self.log.debug("%s: Destroyed by lack of happiness", self) 338 if self.owner.is_local_player: 339 # check_duplicate: only trigger once for different settlers of a neighborhood 340 self.session.ingame_gui.message_widget.add(point=self.position.center, 341 string_id='SETTLERS_MOVED_OUT', check_duplicate=True) 342 else: 343 self.level -= 1 344 self._update_level_data() 345 # reset happiness value for new level 346 new_happiness = self.__get_data("happiness_init_value") - self.happiness 347 self.get_component(StorageComponent).inventory.alter(RES.HAPPINESS, new_happiness) 348 self.log.debug("%s: Level down to %s", self, self.level) 349 self._changed() 350 351 # update the level of our inhabitants so graphics can change 352 if self.has_component(CollectingComponent): 353 for collector in self.get_component(CollectingComponent).get_local_collectors(): 354 collector.level_upgrade(self.level) 355 356 # Notify the world about the level down 357 SettlerUpdate.broadcast(self, self.level, -1)
358
359 - def make_ruin(self):
360 """ Replaces itself with a ruin. 361 """ 362 command = Build(BUILDINGS.SETTLER_RUIN, self.position.origin.x, 363 self.position.origin.y, island=self.island, settlement=self.settlement) 364 365 # Remove the building and then place the Ruin 366 Scheduler().add_new_object(Callback.ChainedCallbacks( 367 self.remove, Callback(command, self.owner)), self, run_in=0)
368
369 - def _has_disaster(self):
370 return hasattr(self, "disaster") and self.disaster
371
372 - def _check_main_square_in_range(self):
373 """Notifies the user via a message in case there is no main square in range""" 374 if not self.owner.is_local_player: 375 return # only check this for local player 376 for building in self.get_buildings_in_range(): 377 if building.id == BUILDINGS.MAIN_SQUARE: 378 if StaticPather.get_path_on_roads(self.island, self, building) is not None: 379 # a main square is in range 380 if hasattr(self, "_main_square_status_icon"): 381 RemoveStatusIcon.broadcast(self, self, SettlerNotConnectedStatus) 382 del self._main_square_status_icon 383 return 384 if not hasattr(self, "_main_square_status_icon"): 385 self._main_square_status_icon = SettlerNotConnectedStatus(self) # save ref for removal later 386 AddStatusIcon.broadcast(self, self._main_square_status_icon) 387 # no main square found 388 # check_duplicate: only trigger once for different settlers of a neighborhood 389 self.session.ingame_gui.message_widget.add(point=self.position.origin, 390 string_id='NO_MAIN_SQUARE_IN_RANGE', check_duplicate=True)
391
392 - def level_upgrade(self, lvl):
393 """Settlers only level up by themselves""" 394 pass
395
396 - def _update_status_icon(self):
397 if self.has_status_icon: 398 unhappy = self.happiness < self.__get_data("happiness_inhabitants_decrease_limit") 399 # check for changes 400 if unhappy and not hasattr(self, "_settler_status_icon"): 401 self._settler_status_icon = SettlerUnhappyStatus(self) # save ref for removal later 402 AddStatusIcon.broadcast(self, self._settler_status_icon) 403 if not unhappy and hasattr(self, "_settler_status_icon"): 404 RemoveStatusIcon.broadcast(self, self, SettlerUnhappyStatus) 405 del self._settler_status_icon
406
407 - def __str__(self):
408 try: 409 return "{}(l:{};ihab:{};hap:{})".format( 410 super().__str__(), self.level, 411 self.inhabitants, self.happiness) 412 except AttributeError: # an attribute hasn't been set up 413 return super().__str__()
414 415 #@decorators.cachedmethod TODO: replace this with a version that doesn't leak
416 - def __get_data(self, key):
417 """Returns constant settler-related data from the db. 418 The values are cached by python, so the underlying data must not change.""" 419 return int( 420 self.session.db("SELECT value FROM balance_values WHERE name = ?", key)[0][0] 421 )
422
423 424 -class SettlerUpgradeData:
425 """This is used as glue between the old upgrade system based on sqlite data used in a non-component environment 426 and the current component version with data in yaml""" 427 428 # basically, this is arbitrary as long as it's not the same as any of the regular 429 # production lines of the settler. We reuse data that has arbitrarily been set earlier 430 # to preserve savegame compatibility. 431 production_line_ids = {1: 24, 2: 35, 3: 23451, 4: 34512, 5: 45123} 432
433 - def __init__(self, producer_component, upgrade_material_data):
434 self.upgrade_material_data = upgrade_material_data
435
436 - def get_production_lines(self):
437 d = {} 438 for level, prod_line_id in self.__class__.production_line_ids.items(): 439 d[prod_line_id] = self.get_production_line_data(level) 440 return d
441
442 - def get_production_line_data(self, level):
443 """Returns production line data for the upgrade to this level""" 444 prod_line_data = {'time': 1, 445 'changes_animation': 0, 446 'enabled_by_default': False, 447 'save_statistics': False, 448 'consumes': self.upgrade_material_data[level]} 449 return prod_line_data
450 451 @classmethod
452 - def get_production_line_id(cls, level):
453 """Returns production line id for the upgrade to this level""" 454 return cls.production_line_ids[level]
455