home · contact · privacy
Make add_line hack unnecessary.
[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 get_surroundings_offset(self):
241         if self._surroundings_offset is not None:
242             return self._surroundings_offset
243         offset = YX(self.position[0].y * self.world.game.map_size.y +
244                     self.position[1].y - self._radius,
245                     self.position[0].x * self.world.game.map_size.x +
246                     self.position[1].x - self._radius)
247         self._surroundings_offset = offset
248         return self._surroundings_offset
249
250     def get_surrounding_map(self):
251         if self._surrounding_map is not None:
252             return self._surrounding_map
253         self._surrounding_map = Map(size=YX(self._radius*2+1, self._radius*2+1))
254         offset = self.get_surroundings_offset()
255         for pos in self._surrounding_map:
256             offset_pos = pos + offset
257             big_yx, small_yx = self.world.game.map_geometry.absolutize_coordinate(self.world.game.map_size, (0,0), offset_pos)
258             map_ = self.world.get_map(big_yx, False)
259             if map_ is None:
260                 map_ = Map(size=self.world.game.map_size)
261             self._surrounding_map[pos] = map_[small_yx]
262         return self._surrounding_map
263
264     def get_stencil(self):
265         if self._stencil is not None:
266             return self._stencil
267         surrounding_map = self.get_surrounding_map()
268         m = Map(surrounding_map.size, ' ')
269         for pos in surrounding_map:
270             if surrounding_map[pos] in {'.', '~'}:
271                 m[pos] = '.'
272         fov_center = YX((m.size.y) // 2, m.size.x // 2)
273         self._stencil = FovMapHex(m, fov_center)
274         return self._stencil
275
276     def get_visible_map(self):
277         stencil = self.get_stencil()
278         m = Map(self.get_surrounding_map().size, ' ')
279         for pos in m:
280             if stencil[pos] == '.':
281                 m[pos] = self._surrounding_map[pos]
282         return m
283
284     def get_visible_things(self):
285         stencil = self.get_stencil()
286         offset = self.get_surroundings_offset()
287         visible_things = []
288         for thing in self.world.things:
289             pos = self.world.game.map_geometry.pos_in_projection(thing.position,
290                                                                  offset,
291                                                                  self.world.game.map_size)
292             if pos.y < 0 or pos.x < 0 or\
293                pos.y >= stencil.size.y or pos.x >= stencil.size.x:
294                 continue
295             if (not thing.in_inventory) and stencil[pos] == '.':
296                 visible_things += [thing]
297         return visible_things
298
299     def get_pickable_items(self):
300         pickable_ids = []
301         visible_things = self.get_visible_things()
302         neighbor_fields = self.world.game.map_geometry.get_neighbors(self.position,
303                                                                      self.world.game.map_size)
304         for t in [t for t in visible_things
305                   if isinstance(t, ThingItem) and
306                   (t.position == self.position or
307                    t.position in neighbor_fields.values())]:
308             pickable_ids += [t.id_]
309         return pickable_ids
310
311
312
313 class ThingHuman(ThingAnimate):
314     type_ = 'human'
315     health = 100
316
317
318
319 class ThingMonster(ThingAnimate):
320     type_ = 'monster'
321     health = 50