1 from plomrogue.errors import GameError, PlayError
2 from plomrogue.mapping import YX
3 from plomrogue.misc import quote
12 def __init__(self, game, id_=0, position=(YX(0, 0), YX(0, 0))):
15 self.id_ = self.game.new_thing_id()
18 self.position = position
22 class Thing(ThingBase):
23 blocks_movement = False
31 def __init__(self, *args, **kwargs):
32 super().__init__(*args, **kwargs)
39 return self.__class__.get_type()
43 return cls.__name__[len('Thing_'):]
45 def sound(self, name, msg):
46 from plomrogue.mapping import DijkstraMap
49 def lower_msg_by_volume(msg, volume, largest_audible_distance,
52 factor = largest_audible_distance / 4
59 in_url = False if in_url else True
61 while random.random() > volume * factor:
64 elif c != '.' and c != ' ':
72 largest_audible_distance = 20
73 obstacles = [t.position for t in self.game.things if t.blocks_sound]
74 sound_blockers = self.game.get_sound_blockers()
75 dijkstra_map = DijkstraMap(sound_blockers, obstacles, self.game.maps,
76 self.position, largest_audible_distance,
79 for m in re.finditer('https?://[^\s]+', msg):
80 url_limits += [m.start(), m.end()]
81 for c_id in self.game.sessions:
82 listener = self.game.get_player(c_id)
83 target_yx = dijkstra_map.target_yx(*listener.position, True)
86 listener_distance = dijkstra_map[target_yx]
87 if listener_distance > largest_audible_distance:
89 volume = 1 / max(1, listener_distance)
90 lowered_msg = lower_msg_by_volume(msg, volume,
91 largest_audible_distance,
93 lowered_nick = lower_msg_by_volume(name, volume,
94 largest_audible_distance)
96 if listener.fov_test(self.position[0], self.position[1]):
97 self.game.io.send('CHATFACE %s' % self.id_, c_id)
98 if self.type_ == 'Player' and hasattr(self, 'thing_char'):
99 symbol = '/@' + self.thing_char
100 self.game.io.send('CHAT ' +
101 quote('vol:%.f%s %s%s: %s' % (volume * 100, '%',
102 lowered_nick, symbol,
108 class Thing_Item(Thing):
114 class ThingSpawner(Thing):
118 for t in [t for t in self.game.things
119 if t != self and t.position == self.position]:
121 self.game.add_thing(self.child_type, self.position)
125 class Thing_ItemSpawner(ThingSpawner):
130 class Thing_SpawnPointSpawner(ThingSpawner):
131 child_type = 'SpawnPoint'
135 class Thing_SpawnPoint(Thing):
142 class Thing_DoorSpawner(ThingSpawner):
147 class Thing_Door(Thing):
149 blocks_movement = False
154 self.blocks_movement = False
155 self.blocks_light = False
156 self.blocks_sound = False
160 self.blocks_movement = True
161 self.blocks_light = True
162 self.blocks_sound = True
163 self.thing_char = '#'
166 self.portable = False
173 class Thing_Bottle(Thing):
181 self.thing_char = '_'
186 all_players = [t for t in self.game.things if t.type_ == 'Player']
187 # TODO: refactor with ThingPlayer.prepare_multiprocessible_fov_stencil
188 # and ThingPlayer.fov_test
189 fov_map_class = self.game.map_geometry.fov_map_class
191 light_blockers = self.game.get_light_blockers()
192 obstacles = [t.position for t in self.game.things if t.blocks_light]
193 fov = fov_map_class(light_blockers, obstacles, self.game.maps,
194 self.position, fov_radius, self.game.get_map)
197 for p in all_players:
198 test_position = fov.target_yx(p.position[0], p.position[1])
199 if fov.inside(test_position) and fov[test_position] == '.':
200 visible_players += [p]
201 if len(visible_players) == 0:
202 self.sound('BOTTLE', 'no visible players in spin range')
203 pick = random.choice(visible_players)
204 self.sound('BOTTLE', 'BOTTLE picks: ' + pick.name)
208 class Thing_BottleSpawner(ThingSpawner):
209 child_type = 'Bottle'
213 class Thing_Hat(Thing):
216 design = ' +--+ ' + ' | | ' + '======'
221 new_design += self.design[12]
222 new_design += self.design[13]
223 new_design += self.design[6]
224 new_design += self.design[7]
225 new_design += self.design[0]
226 new_design += self.design[1]
227 new_design += self.design[14]
228 new_design += self.design[15]
229 new_design += self.design[8]
230 new_design += self.design[9]
231 new_design += self.design[2]
232 new_design += self.design[3]
233 new_design += self.design[16]
234 new_design += self.design[17]
235 new_design += self.design[10]
236 new_design += self.design[11]
237 new_design += self.design[4]
238 new_design += self.design[5]
239 self.design = ''.join(new_design)
243 class Thing_HatRemixer(Thing):
246 def accept(self, hat):
249 legal_chars = string.ascii_letters + string.digits + string.punctuation + ' '
251 new_design += random.choice(list(legal_chars))
252 hat.design = new_design
253 self.sound('HAT REMIXER', 'remixing a hat …')
254 self.game.changed = True
255 self.game.record_change(self.position, 'other')
260 class Thing_MusicPlayer(Thing):
265 next_song_start = datetime.datetime.now()
269 def __init__(self, *args, **kwargs):
270 super().__init__(*args, **kwargs)
271 self.next_song_start = datetime.datetime.now()
275 if (not self.playing) or len(self.playlist) == 0:
277 if datetime.datetime.now() > self.next_song_start:
278 self.playlist_index += 1
279 if self.playlist_index == len(self.playlist):
280 self.playlist_index = 0
284 song_data = self.playlist[self.playlist_index]
285 self.next_song_start = datetime.datetime.now() +\
286 datetime.timedelta(seconds=song_data[1])
287 self.sound('MUSICPLAYER', song_data[0])
288 self.game.changed = True
290 def interpret(self, command):
292 if command == 'HELP':
293 msg_lines += ['available commands:']
294 msg_lines += ['HELP – show this help']
295 msg_lines += ['ON/OFF – toggle playback on/off']
296 msg_lines += ['REWIND – return to start of playlist']
297 msg_lines += ['LIST – list programmed songs, durations']
298 msg_lines += ['SKIP – to skip to next song']
299 msg_lines += ['REPEAT – toggle playlist repeat on/off']
300 msg_lines += ['ADD LENGTH SONG – add SONG to playlist, with LENGTH in format "minutes:seconds", i.e. something like "0:47" or "11:02"']
302 elif command == 'LIST':
303 msg_lines += ['playlist:']
305 for entry in self.playlist:
306 minutes = entry[1] // 60
307 seconds = entry[1] % 60
309 seconds = '0%s' % seconds
310 selector = 'next:' if i == self.playlist_index else ' '
311 msg_lines += ['%s %s:%s – %s' % (selector, minutes, seconds, entry[0])]
314 elif command == 'ON/OFF':
315 self.playing = False if self.playing else True
316 self.game.changed = True
321 elif command == 'REMOVE':
322 if len(self.playlist) == 0:
323 return ['playlist already empty']
324 del self.playlist[max(0, self.playlist_index)]
325 self.playlist_index -= 1
326 if self.playlist_index < -1:
327 self.playlist_index = -1
328 self.game.changed = True
329 return ['removed song']
330 elif command == 'REWIND':
331 self.playlist_index = -1
332 self.next_song_start = datetime.datetime.now()
333 self.game.changed = True
334 return ['back at start of playlist']
335 elif command == 'SKIP':
336 self.next_song_start = datetime.datetime.now()
337 self.game.changed = True
339 elif command == 'REPEAT':
340 self.repeat = False if self.repeat else True
341 self.game.changed = True
343 return ['playlist repeat turned on']
345 return ['playlist repeat turned off']
346 elif command.startswith('ADD '):
347 tokens = command.split(' ', 2)
349 return ['wrong syntax, see HELP']
350 length = tokens[1].split(':')
352 return ['wrong syntax, see HELP']
354 minutes = int(length[0])
355 seconds = int(length[1])
357 return ['wrong syntax, see HELP']
358 self.playlist += [(tokens[2], minutes * 60 + seconds)]
359 self.game.changed = True
362 return ['cannot understand command']
366 class Thing_BottleDeposit(Thing):
371 if self.bottle_counter >= 3:
372 self.bottle_counter = 0
373 choice = random.choice(['MusicPlayer', 'Hat'])
374 self.game.add_thing(choice, self.position)
375 msg = 'here is a gift as a reward for ecological consciousness –'
376 if choice == 'MusicPlayer':
377 msg += 'pick it up and then use "command thing" on it!'
378 elif choice == 'Hat':
379 msg += 'pick it up and then use "(un-)wear" on it!'
380 self.sound('BOTTLE DEPOSITOR', msg)
383 self.bottle_counter += 1
384 self.sound('BOTTLE DEPOSITOR',
385 'thanks for this empty bottle – deposit %s more for a gift!' %
386 (3 - self.bottle_counter))
390 class Thing_Cookie(Thing):
394 def __init__(self, *args, **kwargs):
396 super().__init__(*args, **kwargs)
397 legal_chars = string.ascii_letters + string.digits + string.punctuation + ' '
398 self.thing_char = random.choice(list(legal_chars))
402 class Thing_CookieSpawner(Thing):
405 def accept(self, thing):
406 self.sound('OVEN', '*heat* *brrzt* here\'s a cookie!')
407 self.game.add_thing('Cookie', self.position)
411 class ThingAnimate(Thing):
412 blocks_movement = True
416 def __init__(self, *args, **kwargs):
417 super().__init__(*args, **kwargs)
418 self.next_task = [None]
420 self.invalidate('fov')
421 self.invalidate('other') # currently redundant though
423 def invalidate(self, type_):
426 self._visible_terrain = None
427 self._visible_control = None
428 self.invalidate('other')
429 elif type_ == 'other':
430 self._seen_things = None
431 self._seen_annotation_positions = None
432 self._seen_portal_positions = None
434 def set_next_task(self, task_name, args=()):
435 task_class = self.game.tasks[task_name]
436 self.next_task = [task_class(self, args)]
438 def get_next_task(self):
439 if self.next_task[0]:
440 task = self.next_task[0]
441 self.next_task = [None]
448 for c_id in self.game.sessions:
449 if self.game.sessions[c_id]['thing_id'] == self.id_:
450 # TODO: refactor with self.send_msg
451 self.game.io.send('DEFAULT_COLORS', c_id)
452 self.game.io.send('CHAT "You sober up."', c_id)
453 self.invalidate('fov')
455 self.game.changed = True
456 if self.task is None:
457 self.task = self.get_next_task()
461 except (PlayError, GameError) as e:
465 if self.task.todo <= 0:
467 self.game.changed = True
468 self.task = self.get_next_task()
470 def prepare_multiprocessible_fov_stencil(self):
471 fov_map_class = self.game.map_geometry.fov_map_class
472 fov_radius = 3 if self.drunk > 0 else 12
473 light_blockers = self.game.get_light_blockers()
474 obstacles = [t.position for t in self.game.things if t.blocks_light]
475 self._fov = fov_map_class(light_blockers, obstacles, self.game.maps,
476 self.position, fov_radius, self.game.get_map)
478 def multiprocessible_fov_stencil(self):
479 self._fov.init_terrain()
482 def fov_stencil(self):
485 # due to the pre-multiprocessing in game.send_gamestate,
486 # the following should actually never be called
487 self.prepare_multiprocessible_fov_stencil()
488 self.multiprocessible_fov_stencil()
491 def fov_stencil_make(self):
494 def fov_test(self, big_yx, little_yx):
495 test_position = self.fov_stencil.target_yx(big_yx, little_yx)
496 if self.fov_stencil.inside(test_position):
497 if self.fov_stencil[test_position] == '.':
501 def fov_stencil_map(self, map_type):
503 for yx in self.fov_stencil:
504 if self.fov_stencil[yx] == '.':
505 big_yx, little_yx = self.fov_stencil.source_yxyx(yx)
506 map_ = self.game.get_map(big_yx, map_type)
507 visible_terrain += map_[little_yx]
509 visible_terrain += ' '
510 return visible_terrain
513 def visible_terrain(self):
514 if self._visible_terrain:
515 return self._visible_terrain
516 self._visible_terrain = self.fov_stencil_map('normal')
517 return self._visible_terrain
520 def visible_control(self):
521 if self._visible_control:
522 return self._visible_control
523 self._visible_control = self.fov_stencil_map('control')
524 return self._visible_control
527 def seen_things(self):
528 if self._seen_things is not None:
529 return self._seen_things
530 self._seen_things = [t for t in self.game.things
531 if self.fov_test(*t.position)]
532 return self._seen_things
535 def seen_annotation_positions(self):
536 if self._seen_annotation_positions is not None:
537 return self._seen_annotation_positions
538 self._seen_annotation_positions = []
539 for big_yx in self.game.annotations:
540 for little_yx in [little_yx for little_yx
541 in self.game.annotations[big_yx]
542 if self.fov_test(big_yx, little_yx)]:
543 self._seen_annotation_positions += [(big_yx, little_yx)]
544 return self._seen_annotation_positions
547 def seen_portal_positions(self):
548 if self._seen_portal_positions is not None:
549 return self._seen_portal_positions
550 self._seen_portal_positions = []
551 for big_yx in self.game.portals:
552 for little_yx in [little_yx for little_yx
553 in self.game.portals[big_yx]
554 if self.fov_test(big_yx, little_yx)]:
555 self._seen_portal_positions += [(big_yx, little_yx)]
556 return self._seen_portal_positions
560 class Thing_Player(ThingAnimate):
563 def __init__(self, *args, **kwargs):
564 super().__init__(*args, **kwargs)
567 def send_msg(self, msg):
568 for c_id in self.game.sessions:
569 if self.game.sessions[c_id]['thing_id'] == self.id_:
570 self.game.io.send(msg, c_id)
579 def add_cookie_char(self, c):
580 if not self.name in self.game.players_hat_chars:
581 self.game.players_hat_chars[self.name] = ' #' # default
582 if not c in self.game.players_hat_chars[self.name]:
583 self.game.players_hat_chars[self.name] += c
585 def get_cookie_chars(self):
586 if self.name in self.game.players_hat_chars:
587 return self.game.players_hat_chars[self.name]
588 return ' #' # default