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