home · contact · privacy
Allow player movement beyond central map. Lots of mapping rewrite.
[plomrogue2-experiments] / new / plomrogue / things.py
1 from plomrogue.errors import GameError
2 from plomrogue.mapping import YX, Map, FovMapHex
3
4
5
6 class ThingBase:
7     type_ = '?'
8
9     def __init__(self, world, id_=None, position=(YX(0,0), YX(0,0))):
10         self.world = world
11         if id_ is None:
12             self.id_ = self.world.new_thing_id()
13         else:
14             self.id_ = id_
15         self.position = position
16
17     @property
18     def position(self):
19         return self._position
20
21     def _position_set(self, pos):
22         """Set self._position to pos.
23
24         We use this setter as core to the @position.setter property
25         method due to property setter subclassing not yet working
26         properly, see <https://bugs.python.org/issue14965>. We will
27         therefore super() _position_set instead of @position.setter in
28         subclasses.
29
30         """
31         self._position = pos
32
33     @position.setter
34     def position(self, pos):
35         self._position_set(pos)
36
37
38
39 class Thing(ThingBase):
40     blocking = False
41     in_inventory = False
42
43     def __init__(self, *args, **kwargs):
44         self.inventory = []
45         self._radius = 8
46         super().__init__(*args, **kwargs)
47
48     def proceed(self):
49         pass
50
51     def _position_set(self, pos):
52         super()._position_set(pos)
53         for t_id in self.inventory:
54             t = self.world.get_thing(t_id)
55             t.position = self.position
56         if not self.id_ == self.world.player_id:
57             return
58         edge_left = self.position[1].x - self._radius
59         edge_right = self.position[1].x + self._radius
60         edge_up = self.position[1].y - self._radius
61         edge_down = self.position[1].y + self._radius
62         if edge_left < 0:
63             self.world.get_map(self.position[0] + YX(1,-1))
64             self.world.get_map(self.position[0] + YX(0,-1))
65             self.world.get_map(self.position[0] + YX(-1,-1))
66         if edge_right >= self.world.game.map_size.x:
67             self.world.get_map(self.position[0] + YX(1,1))
68             self.world.get_map(self.position[0] + YX(0,1))
69             self.world.get_map(self.position[0] + YX(-1,1))
70         if edge_up < 0:
71             self.world.get_map(self.position[0] + YX(-1,1))
72             self.world.get_map(self.position[0] + YX(-1,0))
73             self.world.get_map(self.position[0] + YX(-1,-1))
74         if edge_down >= self.world.game.map_size.y:
75             self.world.get_map(self.position[0] + YX(1,1))
76             self.world.get_map(self.position[0] + YX(1,0))
77             self.world.get_map(self.position[0] + YX(1,-1))
78
79
80
81 class ThingItem(Thing):
82     pass
83
84
85
86 class ThingFood(ThingItem):
87     type_ = 'food'
88
89
90
91 class ThingAnimate(Thing):
92     blocking = True
93
94     def __init__(self, *args, **kwargs):
95         super().__init__(*args, **kwargs)
96         self.set_task('WAIT')
97         self._last_task_result = None
98         self.unset_surroundings()
99
100     def move_on_dijkstra_map(self, own_pos, targets):
101         visible_map = self.get_visible_map()
102         dijkstra_map = Map(visible_map.size)
103         n_max = 256
104         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
105         for target in targets:
106             dijkstra_map[target] = 0
107         shrunk = True
108         while shrunk:
109             shrunk = False
110             for pos in dijkstra_map:
111                 if visible_map[pos] != '.':
112                     continue
113                 neighbors = self.world.game.map_geometry.get_neighbors((YX(0,0), pos),
114                                                                        dijkstra_map.size)
115                 for direction in neighbors:
116                     big_yx, small_yx = neighbors[direction]
117                     if big_yx == YX(0,0) and \
118                        dijkstra_map[small_yx] < dijkstra_map[pos] - 1:
119                         dijkstra_map[pos] = dijkstra_map[small_yx] + 1
120                         shrunk = True
121         neighbors = self.world.game.map_geometry.get_neighbors((YX(0,0), own_pos),
122                                                                dijkstra_map.size)
123         n = n_max
124         target_direction = None
125         for direction in sorted(neighbors.keys()):
126             big_yx, small_yx = neighbors[direction]
127             if big_yx == (0,0):
128                 n_new = dijkstra_map[small_yx]
129                 if n_new < n:
130                     n = n_new
131                     target_direction = direction
132         return target_direction
133
134     def hunt_player(self):
135         visible_things = self.get_visible_things()
136         offset = self.get_surroundings_offset()
137         target = None
138         for t in visible_things:
139             if t.type_ == 'human':
140                 target = t.position[1] - offset
141                 break
142         if target is not None:
143             try:
144                 offset_self_pos = self.position[1] - offset
145                 target_dir = self.move_on_dijkstra_map(offset_self_pos,
146                                                        [target])
147                 if target_dir is not None:
148                     self.set_task('MOVE', (target_dir,))
149                     return True
150             except GameError:
151                 pass
152         return False
153
154     def hunt_food_satisfaction(self):
155         for id_ in self.inventory:
156             t = self.world.get_thing(id_)
157             if t.type_ == 'food':
158                 self.set_task('EAT', (id_,))
159                 return True
160         for id_ in self.get_pickable_items():
161             t = self.world.get_thing(id_)
162             if t.type_ == 'food':
163                 self.set_task('PICKUP', (id_,))
164                 return True
165         visible_things = self.get_visible_things()
166         offset = self.get_surroundings_offset()
167         food_targets = []
168         for t in visible_things:
169             if t.type_ == 'food':
170                 food_targets += [t.position[1] - offset]
171         offset_self_pos = self.position[1] - offset
172         target_dir = self.move_on_dijkstra_map(offset_self_pos,
173                                                food_targets)
174         if target_dir:
175             try:
176                 self.set_task('MOVE', (target_dir,))
177                 return True
178             except GameError:
179                 pass
180         return False
181
182     def decide_task(self):
183         #if not self.hunt_player():
184         if not self.hunt_food_satisfaction():
185             self.set_task('WAIT')
186
187     def set_task(self, task_name, args=()):
188         task_class = self.world.game.tasks[task_name]
189         self.task = task_class(self, args)
190         self.task.check()  # will throw GameError if necessary
191
192     def proceed(self, is_AI=True):
193         """Further the thing in its tasks, decrease its health.
194
195         First, ensures an empty map, decrements .health and kills
196         thing if crossing zero (removes from self.world.things for AI
197         thing, or unsets self.world.player_is_alive for player thing);
198         then checks that self.task is still possible and aborts if
199         otherwise (for AI things, decides a new task).
200
201         Then decrements .task.todo; if it thus falls to <= 0, enacts
202         method whose name is 'task_' + self.task.name and sets .task =
203         None. If is_AI, calls .decide_task to decide a self.task.
204
205         """
206         self.unset_surroundings()
207         self.health -= 1
208         if self.health <= 0:
209             if self is self.world.player:
210                 self.world.player_is_alive = False
211             else:
212                 del self.world.things[self.world.things.index(self)]
213             return
214         try:
215             self.task.check()
216         except GameError as e:
217             self.task = None
218             self._last_task_result = e
219             if is_AI:
220                 try:
221                     self.decide_task()
222                 except GameError:
223                     self.set_task('WAIT')
224             return
225         self.task.todo -= 1
226         if self.task.todo <= 0:
227             self._last_task_result = self.task.do()
228             self.task = None
229         if is_AI and self.task is None:
230             try:
231                 self.decide_task()
232             except GameError:
233                 self.set_task('WAIT')
234
235     def unset_surroundings(self):
236         self._stencil = None
237         self._surrounding_map = None
238         self._surroundings_offset = None
239
240     def must_fix_indentation(self):
241         return self._radius % 2 != self.position[1].y % 2
242
243     def get_surroundings_offset(self):
244         if self._surroundings_offset is not None:
245             return self._surroundings_offset
246         add_line = self.must_fix_indentation()
247         offset = YX(self.position[0].y * self.world.game.map_size.y + self.position[1].y - self._radius - int(add_line),
248                     self.position[0].x * self.world.game.map_size.x + self.position[1].x - self._radius)
249         self._surroundings_offset = offset
250         return self._surroundings_offset
251
252     def get_surrounding_map(self):
253         if self._surrounding_map is not None:
254             return self._surrounding_map
255         add_line = self.must_fix_indentation()
256         self._surrounding_map = Map(size=YX(self._radius*2+1+int(add_line),
257                                             self._radius*2+1))
258         offset = self.get_surroundings_offset()
259         for pos in self._surrounding_map:
260             offset_pos = pos + offset
261             big_yx, small_yx = self.world.game.map_geometry.absolutize_coordinate(self.world.game.map_size, (0,0), offset_pos)
262             map_ = self.world.get_map(big_yx, False)
263             if map_ is None:
264                 map_ = Map(size=self.world.game.map_size)
265             self._surrounding_map[pos] = map_[small_yx]
266         return self._surrounding_map
267
268     def get_stencil(self):
269         if self._stencil is not None:
270             return self._stencil
271         surrounding_map = self.get_surrounding_map()
272         m = Map(surrounding_map.size, ' ')
273         for pos in surrounding_map:
274             if surrounding_map[pos] in {'.', '~'}:
275                 m[pos] = '.'
276         add_line = self.must_fix_indentation()
277         fov_center = YX((add_line + m.size.y) // 2, m.size.x // 2)
278         self._stencil = FovMapHex(m, fov_center)
279         return self._stencil
280
281     def get_visible_map(self):
282         stencil = self.get_stencil()
283         m = Map(self.get_surrounding_map().size, ' ')
284         for pos in m:
285             if stencil[pos] == '.':
286                 m[pos] = self._surrounding_map[pos]
287         return m
288
289     def get_visible_things(self):
290         stencil = self.get_stencil()
291         offset = self.get_surroundings_offset()
292         visible_things = []
293         for thing in self.world.things:
294             pos = self.world.game.map_geometry.pos_in_projection(thing.position,
295                                                                  offset,
296                                                                  self.world.game.map_size)
297             if pos.y < 0 or pos.x < 0 or\
298                pos.y >= stencil.size.y or pos.x >= stencil.size.x:
299                 continue
300             if (not thing.in_inventory) and stencil[pos] == '.':
301                 visible_things += [thing]
302         return visible_things
303
304     def get_pickable_items(self):
305         pickable_ids = []
306         visible_things = self.get_visible_things()
307         neighbor_fields = self.world.game.map_geometry.get_neighbors(self.position,
308                                                                      self.world.game.map_size)
309         for t in [t for t in visible_things
310                   if isinstance(t, ThingItem) and
311                   (t.position == self.position or
312                    t.position in neighbor_fields.values())]:
313             pickable_ids += [t.id_]
314         return pickable_ids
315
316
317
318 class ThingHuman(ThingAnimate):
319     type_ = 'human'
320     health = 100
321
322
323
324 class ThingMonster(ThingAnimate):
325     type_ = 'monster'
326     health = 50