From e5a83f8987647c3c239e48d5bc1ff939ce531544 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Thu, 3 Dec 2020 05:26:49 +0100 Subject: [PATCH] Add music player. --- config.json | 1 + plomrogue/commands.py | 59 +++++++----------- plomrogue/game.py | 6 ++ plomrogue/parser.py | 4 ++ plomrogue/tasks.py | 18 ++++++ plomrogue/things.py | 142 ++++++++++++++++++++++++++++++++++++++++++ rogue_chat.html | 24 ++++++- rogue_chat.py | 12 +++- rogue_chat_curses.py | 26 +++++++- 9 files changed, 249 insertions(+), 43 deletions(-) diff --git a/config.json b/config.json index d381939..81d6c11 100644 --- a/config.json +++ b/config.json @@ -7,6 +7,7 @@ "switch_to_study": "?", "switch_to_edit": "E", "switch_to_write": "m", + "switch_to_command_thing": "O", "switch_to_name_thing": "N", "switch_to_admin": "A", "switch_to_control_pw_pw": "C", diff --git a/plomrogue/commands.py b/plomrogue/commands.py index a92359e..0638cb8 100644 --- a/plomrogue/commands.py +++ b/plomrogue/commands.py @@ -22,47 +22,10 @@ def cmd_TERRAINS(game, connection_id): cmd_TERRAINS.argtypes = '' def cmd_ALL(game, msg, connection_id): - from plomrogue.mapping import DijkstraMap - - def lower_msg_by_volume(msg, volume, largest_audible_distance): - import random - factor = largest_audible_distance / 4 - lowered_msg = '' - for c in msg: - c = c - while random.random() > volume * factor: - if c.isupper(): - c = c.lower() - elif c != '.' and c != ' ': - c = '.' - else: - c = ' ' - lowered_msg += c - return lowered_msg - speaker = game.get_player(connection_id) if not speaker: raise GameError('need to be logged in for this') - largest_audible_distance = 20 - things = [t for t in game.things if t.type_ != 'Player'] - dijkstra_map = DijkstraMap(things, game.maps, speaker.position, - largest_audible_distance, game.get_map) - for c_id in game.sessions: - listener = game.get_player(c_id) - target_yx = dijkstra_map.target_yx(*listener.position, True) - if not target_yx: - continue - listener_distance = dijkstra_map[target_yx] - if listener_distance > largest_audible_distance: - continue - volume = 1 / max(1, listener_distance) - lowered_msg = lower_msg_by_volume(msg, volume, largest_audible_distance) - lowered_nick = lower_msg_by_volume(speaker.name, volume, - largest_audible_distance) - game.io.send('CHAT ' + - quote('(volume: %.2f) %s: %s' % (volume, lowered_nick, - lowered_msg)), - c_id) + speaker.sound(speaker.name, msg) cmd_ALL.argtypes = 'string' def cmd_SPAWN_POINT(game, big_yx, little_yx): @@ -322,3 +285,23 @@ def cmd_THING_DOOR_CLOSED(game, thing_id): t.portable = False t.thing_char = '#' cmd_THING_DOOR_CLOSED.argtypes = 'int:pos' + +def cmd_THING_MUSICPLAYER_SETTINGS(game, thing_id, playing, index, repeat): + t = game.get_thing(thing_id) + if not t: + raise GameError('thing of ID %s not found' % thing_id) + if not t.type_ == 'MusicPlayer': + raise GameError('thing of ID %s not music player' % thing_id) + t.playing = playing + t.playlist_index = index + t.repeat = repeat +cmd_THING_MUSICPLAYER_SETTINGS.argtypes = 'int:pos bool int:nonneg bool' + +def cmd_THING_MUSICPLAYER_PLAYLIST_ITEM(game, thing_id, title, length): + t = game.get_thing(thing_id) + if not t: + raise GameError('thing of ID %s not found' % thing_id) + if not t.type_ == 'MusicPlayer': + raise GameError('thing of ID %s not music player' % thing_id) + t.playlist += [(title, length)] +cmd_THING_MUSICPLAYER_PLAYLIST_ITEM.argtypes = 'int:pos string int:pos' diff --git a/plomrogue/game.py b/plomrogue/game.py index bfa521c..ba25eca 100755 --- a/plomrogue/game.py +++ b/plomrogue/game.py @@ -354,6 +354,12 @@ class Game(GameBase): write(f, 'GOD_THING_NAME %s %s' % (t.id_, quote(t.name))) if t.type_ == 'Door' and t.blocking: write(f, 'THING_DOOR_CLOSED %s' % t.id_) + if t.type_ == 'MusicPlayer': + write(f, 'THING_MUSICPLAYER_SETTINGS %s %s %s %s' % + (t.id_, int(t.playing), t.playlist_index, int(t.repeat))) + for item in t.playlist: + write(f, 'THING_MUSICPLAYER_PLAYLIST_ITEM %s %s %s' % + (t.id_, quote(item[0]), item[1])) write(f, 'SPAWN_POINT %s %s' % (self.spawn_point[0], self.spawn_point[1])) diff --git a/plomrogue/parser.py b/plomrogue/parser.py index a043bde..69f728e 100644 --- a/plomrogue/parser.py +++ b/plomrogue/parser.py @@ -116,6 +116,10 @@ class Parser: if not arg.isdigit() or int(arg) < 1: raise ArgError('Argument must be positive integer.') args += [int(arg)] + elif tmpl == 'bool': + if not arg.isdigit() or int(arg) not in (0, 1): + raise ArgError('Argument must be 0 or 1.') + args += [bool(int(arg))] elif tmpl == 'char': try: ord(arg) diff --git a/plomrogue/tasks.py b/plomrogue/tasks.py index 8b83131..0bc0d9c 100644 --- a/plomrogue/tasks.py +++ b/plomrogue/tasks.py @@ -139,4 +139,22 @@ class Task_INTOXICATE(Task): if self.thing.game.sessions[c_id]['thing_id'] == self.thing.id_: self.thing.game.io.send('RANDOM_COLORS', c_id) self.thing.game.io.send('CHAT "You are drunk now."', c_id) + break self.thing.drunk = 10000 + + +class Task_COMMAND(Task): + argtypes = 'string' + + def check(self): + if self.thing.carrying is None: + raise PlayError('nothing to command') + if not self.thing.carrying.commandable: + raise PlayError('cannot command this item type') + + def do(self): + from plomrogue.misc import quote + reply = self.thing.carrying.interpret(self.args[0]) + for c_id in self.thing.game.sessions: + if self.thing.game.sessions[c_id]['thing_id'] == self.thing.id_: + self.thing.game.io.send('REPLY ' + quote(reply), c_id) diff --git a/plomrogue/things.py b/plomrogue/things.py index 2146c1a..15216ef 100644 --- a/plomrogue/things.py +++ b/plomrogue/things.py @@ -20,6 +20,7 @@ class Thing(ThingBase): blocking = False portable = False protection = '.' + commandable = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -35,6 +36,50 @@ class Thing(ThingBase): def get_type(cls): return cls.__name__[len('Thing_'):] + def sound(self, name, msg): + from plomrogue.mapping import DijkstraMap + from plomrogue.misc import quote + + def lower_msg_by_volume(msg, volume, largest_audible_distance): + import random + factor = largest_audible_distance / 4 + lowered_msg = '' + for c in msg: + c = c + while random.random() > volume * factor: + if c.isupper(): + c = c.lower() + elif c != '.' and c != ' ': + c = '.' + else: + c = ' ' + lowered_msg += c + return lowered_msg + + largest_audible_distance = 20 + # player's don't block sound (or should they?) + things = [t for t in self.game.things if t.type_ != 'Player'] + dijkstra_map = DijkstraMap(things, self.game.maps, self.position, + largest_audible_distance, self.game.get_map) + for c_id in self.game.sessions: + listener = self.game.get_player(c_id) + target_yx = dijkstra_map.target_yx(*listener.position, True) + if not target_yx: + continue + listener_distance = dijkstra_map[target_yx] + if listener_distance > largest_audible_distance: + continue + volume = 1 / max(1, listener_distance) + lowered_msg = lower_msg_by_volume(msg, volume, + largest_audible_distance) + lowered_nick = lower_msg_by_volume(name, volume, + largest_audible_distance) + self.game.io.send('CHAT ' + + quote('(volume: %.2f) %s: %s' % (volume, + lowered_nick, + lowered_msg)), + c_id) + class Thing_Item(Thing): @@ -107,6 +152,103 @@ class Thing_ConsumableSpawner(ThingSpawner): +import datetime +class Thing_MusicPlayer(Thing): + symbol_hint = 'R' + commandable = True + portable = True + playlist = [] + repeat = True + next_song_start = datetime.datetime.now() + playlist_index = 0 + playing = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.next_song_start = datetime.datetime.now() + + def proceed(self): + if (not self.playing) or len(self.playlist) == 0: + return + if datetime.datetime.now() > self.next_song_start: + song_data = self.playlist[self.playlist_index] + self.playlist_index += 1 + if self.playlist_index == len(self.playlist): + self.playlist_index = 0 + if not self.repeat: + self.playing = False + self.next_song_start = datetime.datetime.now() +\ + datetime.timedelta(seconds=song_data[1]) + self.sound('MUSICPLAYER', song_data[0]) + self.game.changed = True + + def interpret(self, command): + if command == 'HELP': + msg = 'available commands:\n' + msg += 'HELP – show this help\n' + msg += 'PLAY – toggle playback on/off\n' + msg += 'REWIND – return to start of playlist\n' + msg += 'LIST – list programmed songs, durations\n' + msg += 'SKIP – to skip to next song\n' + msg += 'REPEAT – toggle playlist repeat on/off\n' + msg += 'ADD LENGTH SONG – add SONG to playlist, with LENGTH in format "minutes:seconds", i.e. something like "0:47" or "11:02"' + return msg + elif command == 'LIST': + msg = 'playlist:' + i = 0 + for entry in self.playlist: + msg += '\n' + minutes = entry[1] // 60 + seconds = entry[1] % 60 + if seconds < 10: + seconds = '0%s' % seconds + selector = 'next:' if i == self.playlist_index else ' ' + msg += '%s %s:%s – %s' % (selector, minutes, seconds, entry[0]) + i += 1 + return msg + elif command == 'PLAY': + self.playing = False if self.playing else True + self.game.changed = True + if self.playing: + return 'playing' + else: + return 'paused' + elif command == 'REWIND': + self.playlist_index = 0 + self.next_song_start = datetime.datetime.now() + self.game.changed = True + return 'back at start of playlist' + elif command == 'SKIP': + self.next_song_start = datetime.datetime.now() + self.game.changed = True + return 'skipped' + elif command == 'REPEAT': + self.repeat = False if self.repeat else True + self.game.changed = True + if self.repeat: + return 'playlist repeat turned on' + else: + return 'playlist repeat turned off' + elif command.startswith('ADD '): + tokens = command.split(' ', 2) + if len(tokens) != 3: + return 'wrong syntax, see HELP' + length = tokens[1].split(':') + if len(length) != 2: + return 'wrong syntax, see HELP' + try: + minutes = int(length[0]) + seconds = int(length[1]) + except ValueError: + return 'wrong syntax, see HELP' + self.playlist += [(tokens[2], minutes * 60 + seconds)] + self.game.changed = True + return 'added' + else: + return 'cannot understand command' + + + class ThingAnimate(Thing): blocking = True drunk = 0 diff --git a/rogue_chat.html b/rogue_chat.html index cc505bc..8efe2a2 100644 --- a/rogue_chat.html +++ b/rogue_chat.html @@ -55,6 +55,7 @@ keyboard input/control: + @@ -105,6 +106,7 @@ keyboard input/control:
  • +
  • @@ -137,6 +139,10 @@ let mode_helps = { 'short': 'name thing', 'long': 'Give name to/change name of thing here.' }, + 'command_thing': { + 'short': 'command thing', + 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.' + }, 'admin_thing_protect': { 'short': 'change thing protection', 'long': 'Change protection character for thing here.' @@ -453,6 +459,7 @@ let server = { } else if (tokens[0] === 'TASKS') { game.tasks = tokens[1].split(','); tui.mode_write.legal = game.tasks.includes('WRITE'); + tui.mode_command_thing.legal = game.tasks.includes('WRITE'); } else if (tokens[0] === 'THING_TYPE') { game.thing_types[tokens[1]] = tokens[2] } else if (tokens[0] === 'TERRAIN') { @@ -476,6 +483,8 @@ let server = { tui.full_refresh(); } else if (tokens[0] === 'CHAT') { tui.log_msg('# ' + tokens[1], 1); + } else if (tokens[0] === 'REPLY') { + tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1); } else if (tokens[0] === 'PLAYER_ID') { game.player_id = parseInt(tokens[1]); } else if (tokens[0] === 'LOGIN_OK') { @@ -614,6 +623,7 @@ let tui = { mode_portal: new Mode('portal', true, true), mode_password: new Mode('password', true), mode_name_thing: new Mode('name_thing', true, true), + mode_command_thing: new Mode('command_thing', true), mode_admin_enter: new Mode('admin_enter', true), mode_admin: new Mode('admin'), mode_control_pw_pw: new Mode('control_pw_pw', true), @@ -625,13 +635,15 @@ let tui = { 'drop_thing': 'DROP', 'move': 'MOVE', 'door': 'DOOR', + 'command': 'COMMAND', 'consume': 'INTOXICATE', }, offset: [0,0], map_lines: [], init: function() { this.mode_chat.available_modes = ["play", "study", "edit", "admin_enter"] - this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"] + this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter", + "command_thing"] this.mode_play.available_actions = ["move", "take_thing", "drop_thing", "teleport", "door", "consume"]; this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"] @@ -764,6 +776,8 @@ let tui = { } } else if (this.mode.is_single_char_entry) { this.show_help = true; + } else if (this.mode.name == 'command_thing') { + server.send(['TASK:COMMAND', 'HELP']); } else if (this.mode.name == 'admin_enter') { this.log_msg('@ enter admin password:') } else if (this.mode.name == 'control_pw_type') { @@ -1324,6 +1338,14 @@ tui.inputEl.addEventListener('keydown', (event) => { tui.login_name = tui.inputEl.value; server.send(['LOGIN', tui.inputEl.value]); tui.inputEl.value = ""; + } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') { + if (tui.inputEl.value.length == 0) { + tui.log_msg('@ aborted'); + tui.switch_mode('play'); + } else if (tui.task_action_on('command')) { + server.send(['TASK:COMMAND', tui.inputEl.value]); + tui.inputEl.value = ""; + } } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') { if (tui.inputEl.value.length == 0) { tui.log_msg('@ aborted'); diff --git a/rogue_chat.py b/rogue_chat.py index 3d9bf18..076fd92 100755 --- a/rogue_chat.py +++ b/rogue_chat.py @@ -9,14 +9,16 @@ from plomrogue.commands import (cmd_ALL, cmd_LOGIN, cmd_NICK, cmd_PING, cmd_THIN cmd_BECOME_ADMIN, cmd_SET_TILE_CONTROL, cmd_GOD_THING_NAME, cmd_THING_DOOR_CLOSED, cmd_GOD_THING_PROTECTION, cmd_THING_PROTECTION, - cmd_SET_MAP_CONTROL_PASSWORD, cmd_SPAWN_POINT) + cmd_SET_MAP_CONTROL_PASSWORD, cmd_SPAWN_POINT, + cmd_THING_MUSICPLAYER_SETTINGS, + cmd_THING_MUSICPLAYER_PLAYLIST_ITEM) from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_WRITE, Task_PICK_UP, Task_DROP, Task_FLATTEN_SURROUNDINGS, Task_DOOR, - Task_INTOXICATE) + Task_INTOXICATE, Task_COMMAND) from plomrogue.things import (Thing_Player, Thing_Item, Thing_ItemSpawner, Thing_SpawnPoint, Thing_SpawnPointSpawner, Thing_Door, Thing_DoorSpawner, Thing_Consumable, - Thing_ConsumableSpawner) + Thing_ConsumableSpawner, Thing_MusicPlayer) from plomrogue.config import config game = Game(config['savefile']) @@ -49,6 +51,8 @@ game.register_command(cmd_SET_TILE_CONTROL) game.register_command(cmd_SET_MAP_CONTROL_PASSWORD) game.register_command(cmd_BECOME_ADMIN) game.register_command(cmd_SPAWN_POINT) +game.register_command(cmd_THING_MUSICPLAYER_SETTINGS) +game.register_command(cmd_THING_MUSICPLAYER_PLAYLIST_ITEM) game.register_task(Task_WAIT) game.register_task(Task_MOVE) game.register_task(Task_WRITE) @@ -57,6 +61,7 @@ game.register_task(Task_PICK_UP) game.register_task(Task_DROP) game.register_task(Task_DOOR) game.register_task(Task_INTOXICATE) +game.register_task(Task_COMMAND) game.register_thing_type(Thing_Player) game.register_thing_type(Thing_Item) game.register_thing_type(Thing_ItemSpawner) @@ -66,6 +71,7 @@ game.register_thing_type(Thing_Door) game.register_thing_type(Thing_DoorSpawner) game.register_thing_type(Thing_Consumable) game.register_thing_type(Thing_ConsumableSpawner) +game.register_thing_type(Thing_MusicPlayer) game.read_savefile() game.io.start_loop() for port in config['servers']: diff --git a/rogue_chat_curses.py b/rogue_chat_curses.py index 2783e24..420c858 100755 --- a/rogue_chat_curses.py +++ b/rogue_chat_curses.py @@ -27,6 +27,10 @@ mode_helps = { 'short': 'name thing', 'long': 'Give name to/change name of thing here.' }, + 'command_thing': { + 'short': 'command thing', + 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.' + }, 'admin_thing_protect': { 'short': 'change thing protection', 'long': 'Change protection character for thing here.' @@ -151,6 +155,11 @@ def cmd_ADMIN_OK(game): game.tui.do_refresh = True cmd_ADMIN_OK.argtypes = '' +def cmd_REPLY(game, msg): + game.tui.log_msg('#MUSICPLAYER: ' + msg) + game.tui.do_refresh = True +cmd_REPLY.argtypes = 'string' + def cmd_CHAT(game, msg): game.tui.log_msg('# ' + msg) game.tui.do_refresh = True @@ -254,6 +263,7 @@ cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string' def cmd_TASKS(game, tasks_comma_separated): game.tasks = tasks_comma_separated.split(',') game.tui.mode_write.legal = 'WRITE' in game.tasks + game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks cmd_TASKS.argtypes = 'string' def cmd_THING_TYPE(game, thing_type, symbol_hint): @@ -287,6 +297,7 @@ class Game(GameBase): self.register_command(cmd_ADMIN_OK) self.register_command(cmd_PONG) self.register_command(cmd_CHAT) + self.register_command(cmd_REPLY) self.register_command(cmd_PLAYER_ID) self.register_command(cmd_TURN) self.register_command(cmd_THING) @@ -385,13 +396,15 @@ class TUI: mode_post_login_wait = Mode('post_login_wait', is_intro=True) mode_password = Mode('password', has_input_prompt=True) mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True) + mode_command_thing = Mode('command_thing', has_input_prompt=True) is_admin = False tile_draw = False def __init__(self, host): import os import json - self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"] + self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter", + "command_thing"] self.mode_play.available_actions = ["move", "take_thing", "drop_thing", "teleport", "door", "consume"] self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"] @@ -429,6 +442,7 @@ class TUI: 'switch_to_edit': 'E', 'switch_to_write': 'm', 'switch_to_name_thing': 'N', + 'switch_to_command_thing': 'O', 'switch_to_admin_enter': 'A', 'switch_to_control_pw_type': 'C', 'switch_to_control_tile_type': 'Q', @@ -594,6 +608,8 @@ class TUI: self.send('LOGIN ' + quote(self.login_name)) else: self.log_msg('@ enter username') + elif self.mode.name == 'command_thing': + self.send('TASK:COMMAND ' + quote('HELP')) elif self.mode.name == 'admin_enter': self.log_msg('@ enter admin password:') elif self.mode.name == 'control_pw_type': @@ -898,6 +914,7 @@ class TUI: 'drop_thing': 'DROP', 'door': 'DOOR', 'move': 'MOVE', + 'command': 'COMMAND', 'consume': 'INTOXICATE', } @@ -960,6 +977,13 @@ class TUI: self.login_name = self.input_ self.send('LOGIN ' + quote(self.input_)) self.input_ = "" + elif self.mode.name == 'command_thing' and key == '\n': + if self.input_ == '': + self.log_msg('@ aborted') + self.switch_mode('play') + elif task_action_on('command'): + self.send('TASK:COMMAND ' + quote(self.input_)) + self.input_ = "" elif self.mode.name == 'control_pw_pw' and key == '\n': if self.input_ == '': self.log_msg('@ aborted') -- 2.30.2