1#file: location.py 2#Copyright (C) 2008 FunnyMan3595 3#This file is part of Endgame: Singularity. 4 5#Endgame: Singularity is free software; you can redistribute it and/or modify 6#it under the terms of the GNU General Public License as published by 7#the Free Software Foundation; either version 2 of the License, or 8#(at your option) any later version. 9 10#Endgame: Singularity is distributed in the hope that it will be useful, 11#but WITHOUT ANY WARRANTY; without even the implied warranty of 12#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13#GNU General Public License for more details. 14 15#You should have received a copy of the GNU General Public License 16#along with Endgame: Singularity; if not, write to the Free Software 17#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 19#This file contains the Location class. 20 21from __future__ import absolute_import 22 23from singularity.code import g, prerequisite, base 24from singularity.code.spec import GenericSpec, SpecDataField, validate_must_be_list, promote_to_list 25from singularity.code.buyable import cash, cpu, labor, SPEC_FIELD_PREREQUISITES 26 27 28def position_data_parser(raw_value): 29 validate_must_be_list(raw_value) 30 abs = False 31 if len(raw_value) == 3: 32 if raw_value[0] != 'absolute': 33 raise ValueError('First element for a 3-element position data must be "absolute", got: %s' % raw_value[0]) 34 abs = True 35 _, x, y = raw_value 36 elif len(raw_value) == 2: 37 x, y = raw_value 38 else: 39 raise ValueError("Location position data must be exactly 2 or 3 elements") 40 return abs, int(x) / -100., int(y) / -100. 41 42 43class LocationSpec(GenericSpec, prerequisite.Prerequisite): 44 45 # The name of this location (loaded dynamically from locations_str.dat) 46 name = "" 47 48 # The hotkey used to open this location (loaded dynamically from locations_str.dat) 49 hotkey = "" 50 51 # The names of cities/places in the location (loaded dynamically from locations_str.dat) 52 cities = [] 53 54 spec_data_fields = [ 55 SpecDataField('position_data', data_field_name="position", converter=position_data_parser), 56 SpecDataField('safety', converter=int, default_value=0), 57 SpecDataField('region', converter=promote_to_list, default_value=list), 58 SpecDataField('modifier', converter=g.read_modifiers_dict, default_value=dict), 59 SPEC_FIELD_PREREQUISITES, 60 ] 61 62 def __init__(self, id, position_data, safety, region, modifier, prerequisites): 63 GenericSpec.__init__(self, id) 64 prerequisite.Prerequisite.__init__(self, prerequisites) 65 self.id = id 66 67 self.absolute, self.x, self.y = position_data 68 self.regions = region 69 self.safety = safety 70 self.modifiers = modifier 71 72 73class Location(object): 74 75 def __init__(self, location_spec, regions): 76 self.spec = location_spec 77 78 # A list of the bases at this location. Often sorted for the GUI. 79 self.bases = [] 80 self.regions = regions 81 self._region_modifiers = None 82 # The cache of the bonus and penalties combined for individual sources 83 self._modifiers_cache = None 84 85 @property 86 def id(self): 87 return self.spec.id 88 89 @property 90 def name(self): 91 return self.spec.name 92 93 def available(self): 94 return self.spec.available() 95 96 @property 97 def x(self): 98 return self.spec.x 99 100 @property 101 def y(self): 102 return self.spec.y 103 104 @property 105 def absolute(self): 106 return self.spec.absolute 107 108 @property 109 def safety(self): 110 return self.spec.safety 111 112 @property 113 def cities(self): 114 return self.spec.cities 115 116 @property 117 def hotkey(self): 118 return self.spec.hotkey 119 120 @staticmethod 121 def _merge_location_modifiers_inplace(current_modifier, *merge_list): 122 for modifier_to_merge in merge_list: 123 for mod_id, mod_value in modifier_to_merge.items(): 124 current_value = current_modifier.get(mod_id, 1) 125 current_value *= mod_value 126 current_modifier[mod_id] = current_value 127 128 def _get_region_modifiers(self): 129 if self._region_modifiers is None: 130 self._region_modifiers = {} 131 Location._merge_location_modifiers_inplace(self._region_modifiers, 132 *[r.modifier_by_location[self.id] for r in self.regions] 133 ) 134 return self._region_modifiers 135 136 @property 137 def modifiers(self): 138 if self._modifiers_cache is None: 139 self._modifiers_cache = {} 140 region_modifiers = self._get_region_modifiers() 141 Location._merge_location_modifiers_inplace(self._modifiers_cache, 142 region_modifiers, 143 self.spec.modifiers 144 ) 145 return self._modifiers_cache 146 147 had_last_discovery = property(lambda self: g.pl.last_discovery == self) 148 had_prev_discovery = property(lambda self: g.pl.prev_discovery == self) 149 150 def discovery_bonus(self): 151 discovery_bonus = 1 152 if self.had_last_discovery: 153 discovery_bonus *= 1.2 154 if self.had_prev_discovery: 155 discovery_bonus *= 1.1 156 if "stealth" in self.modifiers: 157 discovery_bonus /= self.modifiers["stealth"] 158 return int(discovery_bonus * 100) 159 160 def modify_cost(self, cost): 161 if "thrift" in self.modifiers: 162 mod = self.modifiers["thrift"] 163 164 # Invert it and apply to the CPU/cash cost. 165 cost[cash] = int(cost[cash] / mod) 166 cost[cpu] = int(cost[cpu] / mod) 167 168 if "speed" in self.modifiers: 169 mod = self.modifiers["speed"] 170 171 # Invert it and apply to the labor cost. 172 cost[labor] = int(cost[labor] / mod) 173 174 def modify_maintenance(self, maintenance): 175 if "thrift" in self.modifiers: 176 mod = self.modifiers["thrift"] 177 178 # Invert it and apply to the CPU/cash maintenance. 179 maintenance[cash] = int(maintenance[cash] / mod) 180 maintenance[cpu] = int(maintenance[cpu] / mod) 181 182 def add_base(self, base): 183 self.bases.append(base) 184 base.location = self 185 186 self.modify_base(base) 187 188 # Make sure the location's CPU modifier is applied. 189 base.recalc_cpu() 190 191 def modify_base(self, base): 192 self.modify_cost(base.total_cost) 193 self.modify_cost(base.cost_left) 194 self.modify_maintenance(base.maintenance) 195 196 def has_modifiers(self): 197 return len(self.modifiers) > 0 198 199 def __eq__(self, other): 200 if self is other: 201 return True 202 if other is None or not isinstance(other, self.__class__): 203 return False 204 return self.id == other.id 205 206 def __hash__(self): 207 return hash(self.id) 208 209 def __lt__(self, other): 210 return self.id < other.id 211 212 def serialize_obj(self): 213 obj_data = { 214 'id': g.to_internal_id('location', self.spec.id), 215 'bases': [b.serialize_obj() for b in self.bases], 216 } 217 return obj_data 218 219 @classmethod 220 def deserialize_obj(cls, obj_data, game_version): 221 spec_id = g.convert_internal_id('location', obj_data['id']) 222 spec = g.locations[spec_id] 223 regions = [g.pl.regions[region_id] for region_id in spec.regions] 224 loc = Location(spec, regions) 225 226 loc.bases = [] 227 bases = obj_data.get('bases', []) 228 for base_obj_data in bases: 229 base_obj = base.Base.deserialize_obj(base_obj_data, game_version) 230 loc.add_base(base_obj) 231 return loc 232 233 def get_modifiers_info(self): 234 modifier_names = { 235 "cpu" : _("CPU"), 236 "stealth" : _("STEALTH"), 237 "speed" : _("BUILDING"), 238 "thrift" : _("COST"), 239 } 240 241 modifiers = [] 242 243 for modifier_id, modifier_value in self.modifiers.items(): 244 if (modifier_value > 1): 245 modifiers.append(_("{MODIFIER} BONUS").format(MODIFIER=modifier_names[modifier_id])) 246 elif (modifier_value < 1): 247 modifiers.append(_("{MODIFIER} MALUS").format(MODIFIER=modifier_names[modifier_id])) 248 249 return ", ".join(modifiers) 250