1 from plomrogue.errors import GameError
2 from plomrogue.mapping import YX, Map, FovMapHex
9 def __init__(self, world, id_=None, position=(YX(0,0), YX(0,0))):
12 self.id_ = self.world.new_thing_id()
15 self.position = position
21 def _position_set(self, pos):
22 """Set self._position to pos.
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
34 def position(self, pos):
35 self._position_set(pos)
39 class Thing(ThingBase):
43 def __init__(self, *args, **kwargs):
46 super().__init__(*args, **kwargs)
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:
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
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))
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))
81 class ThingItem(Thing):
86 class ThingFood(ThingItem):
91 class ThingAnimate(Thing):
94 def __init__(self, *args, **kwargs):
95 super().__init__(*args, **kwargs)
97 self._last_task_result = None
98 self.unset_surroundings()
100 def move_on_dijkstra_map(self, own_pos, targets):
101 visible_map = self.get_visible_map()
102 dijkstra_map = Map(visible_map.size)
104 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
105 for target in targets:
106 dijkstra_map[target] = 0
110 for pos in dijkstra_map:
111 if visible_map[pos] != '.':
113 neighbors = self.world.game.map_geometry.get_neighbors((YX(0,0), pos),
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
121 neighbors = self.world.game.map_geometry.get_neighbors((YX(0,0), own_pos),
124 target_direction = None
125 for direction in sorted(neighbors.keys()):
126 big_yx, small_yx = neighbors[direction]
128 n_new = dijkstra_map[small_yx]
131 target_direction = direction
132 return target_direction
134 def hunt_player(self):
135 visible_things = self.get_visible_things()
136 offset = self.get_surroundings_offset()
138 for t in visible_things:
139 if t.type_ == 'human':
140 target = t.position[1] - offset
142 if target is not None:
144 offset_self_pos = self.position[1] - offset
145 target_dir = self.move_on_dijkstra_map(offset_self_pos,
147 if target_dir is not None:
148 self.set_task('MOVE', (target_dir,))
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_,))
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_,))
165 visible_things = self.get_visible_things()
166 offset = self.get_surroundings_offset()
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,
176 self.set_task('MOVE', (target_dir,))
182 def decide_task(self):
183 #if not self.hunt_player():
184 if not self.hunt_food_satisfaction():
185 self.set_task('WAIT')
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
192 def proceed(self, is_AI=True):
193 """Further the thing in its tasks, decrease its health.
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).
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.
206 self.unset_surroundings()
209 if self is self.world.player:
210 self.world.player_is_alive = False
212 del self.world.things[self.world.things.index(self)]
216 except GameError as e:
218 self._last_task_result = e
223 self.set_task('WAIT')
226 if self.task.todo <= 0:
227 self._last_task_result = self.task.do()
229 if is_AI and self.task is None:
233 self.set_task('WAIT')
235 def unset_surroundings(self):
237 self._surrounding_map = None
238 self._surroundings_offset = None
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
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)
260 map_ = Map(size=self.world.game.map_size)
261 self._surrounding_map[pos] = map_[small_yx]
262 return self._surrounding_map
264 def get_stencil(self):
265 if self._stencil is not None:
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 {'.', '~'}:
272 fov_center = YX((m.size.y) // 2, m.size.x // 2)
273 self._stencil = FovMapHex(m, fov_center)
276 def get_visible_map(self):
277 stencil = self.get_stencil()
278 m = Map(self.get_surrounding_map().size, ' ')
280 if stencil[pos] == '.':
281 m[pos] = self._surrounding_map[pos]
284 def get_visible_things(self):
285 stencil = self.get_stencil()
286 offset = self.get_surroundings_offset()
288 for thing in self.world.things:
289 pos = self.world.game.map_geometry.pos_in_projection(thing.position,
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:
295 if (not thing.in_inventory) and stencil[pos] == '.':
296 visible_things += [thing]
297 return visible_things
299 def get_pickable_items(self):
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_]
313 class ThingHuman(ThingAnimate):
319 class ThingMonster(ThingAnimate):