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 must_fix_indentation(self):
241 return self._radius % 2 != self.position[1].y % 2
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
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),
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)
264 map_ = Map(size=self.world.game.map_size)
265 self._surrounding_map[pos] = map_[small_yx]
266 return self._surrounding_map
268 def get_stencil(self):
269 if self._stencil is not None:
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 {'.', '~'}:
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)
281 def get_visible_map(self):
282 stencil = self.get_stencil()
283 m = Map(self.get_surrounding_map().size, ' ')
285 if stencil[pos] == '.':
286 m[pos] = self._surrounding_map[pos]
289 def get_visible_things(self):
290 stencil = self.get_stencil()
291 offset = self.get_surroundings_offset()
293 for thing in self.world.things:
294 pos = self.world.game.map_geometry.pos_in_projection(thing.position,
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:
300 if (not thing.in_inventory) and stencil[pos] == '.':
301 visible_things += [thing]
302 return visible_things
304 def get_pickable_items(self):
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_]
318 class ThingHuman(ThingAnimate):
324 class ThingMonster(ThingAnimate):