diff --git a/clients/desktop/ui/main_window.py b/clients/desktop/ui/main_window.py index ceeeb772..5a48a80b 100644 --- a/clients/desktop/ui/main_window.py +++ b/clients/desktop/ui/main_window.py @@ -263,6 +263,7 @@ def _setup_accelerators(self): self.ID_PING = wx.NewIdRef() self.ID_LIST_ONLINE = wx.NewIdRef() self.ID_LIST_ONLINE_WITH_GAMES = wx.NewIdRef() + self.ID_F1 = wx.NewIdRef() # Buffer system IDs self.ID_PREV_BUFFER = wx.NewIdRef() @@ -278,6 +279,7 @@ def _setup_accelerators(self): # Common accelerators that work everywhere common_entries = [ wx.AcceleratorEntry(wx.ACCEL_ALT, ord("M"), self.ID_FOCUS_MENU), + wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_F1, self.ID_F1), wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_F6, self.ID_TOGGLE_TABLE_CHAT), wx.AcceleratorEntry(wx.ACCEL_SHIFT, wx.WXK_F6, self.ID_TOGGLE_GLOBAL_CHAT), wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_F7, self.ID_AMBIENCE_DOWN), @@ -327,6 +329,7 @@ def _setup_accelerators(self): self.on_list_online_with_games, id=self.ID_LIST_ONLINE_WITH_GAMES, ) + self.Bind(wx.EVT_MENU, self.on_f1_keybind, id=self.ID_F1) # Buffer system event bindings self.Bind(wx.EVT_MENU, self.on_prev_buffer, id=self.ID_PREV_BUFFER) @@ -372,6 +375,11 @@ def on_menu_focus(self, event): self.SetAcceleratorTable(self.accel_table_with_buffers) event.Skip() + def on_f1_keybind(self, event): + """Handle F1 accelerator (global rules request).""" + if self.connected: + self.network.send_packet({"type": "keybind", "key": "f1"}) + def on_menu_unfocus(self, event): """Handle menu list losing focus - disable buffer navigation.""" try: @@ -693,11 +701,10 @@ def _map_direction_key(self, event, key_code: int, menu_is_empty: bool) -> str | def _map_function_key(self, event, key_code: int) -> str | None: key_map = { - wx.WXK_F1: "f1", wx.WXK_F3: "f3", wx.WXK_F5: "f5", } - if key_code in (wx.WXK_F2, wx.WXK_F4): + if key_code in (wx.WXK_F1, wx.WXK_F2, wx.WXK_F4): event.Skip() return None return key_map.get(key_code) @@ -730,7 +737,6 @@ def _send_keybind_if_allowed(self, event, key_name: str) -> bool: has_shift = (modifiers & wx.MOD_SHIFT) != 0 is_function_key = key_name in [ - "f1", "f2", "f3", "f5", diff --git a/server/core/server.py b/server/core/server.py index 311dc1ea..21d31895 100644 --- a/server/core/server.py +++ b/server/core/server.py @@ -4114,7 +4114,8 @@ async def _handle_keybind(self, client: ClientConnection, packet: dict) -> None: user = self._users.get(username) table = self._tables.find_user_table(username) if not table and user: - if packet.get("key") == "w" and packet.get("control"): + keybind_key = packet.get("key") + if keybind_key == "w" and packet.get("control"): players = [ u.username for u in self._users.values() @@ -4126,6 +4127,8 @@ async def _handle_keybind(self, client: ClientConnection, packet: dict) -> None: names = Localization.format_list_and(user.locale, players) key = "online-users-one" if len(players) == 1 else "online-users-many" user.speak_l(key, count=len(players), users=names) + elif keybind_key == "f1": + user.speak_l("action-must-be-at-table") return if table and table.game and user: player = table.game.get_player_by_id(user.uuid) diff --git a/server/core/tables/table.py b/server/core/tables/table.py index 8832f8db..27aa0b44 100644 --- a/server/core/tables/table.py +++ b/server/core/tables/table.py @@ -98,6 +98,10 @@ def attach_user(self, username: str, user: "User") -> None: """Attach a user to a member (e.g., after deserialization).""" self._users[username] = user + def get_documents(self) -> Any: + """Get the server's document manager.""" + return self._server._documents + def get_players(self) -> list[TableMember]: """Get all non-spectator members.""" return [m for m in self.members if not m.is_spectator] diff --git a/server/game_utils/action_set_creation_mixin.py b/server/game_utils/action_set_creation_mixin.py index 5d6fdd42..7b1929ce 100644 --- a/server/game_utils/action_set_creation_mixin.py +++ b/server/game_utils/action_set_creation_mixin.py @@ -102,11 +102,21 @@ def create_standard_action_set(self, player: "Player") -> ActionSet: id="show_actions", label=Localization.get(locale, "actions-menu"), handler="_action_show_actions_menu", - is_enabled="_is_show_actions_enabled", + is_enabled="_is_always_enabled", is_hidden="_is_always_hidden", show_in_actions_menu=False, ) ) + action_set.add( + Action( + id="show_rules", + label=Localization.get(locale, "show-rules"), + handler="_action_show_rules", + is_enabled="_is_always_enabled", + is_hidden="_is_always_hidden", + show_in_actions_menu=True, + ) + ) action_set.add( Action( id="save_table", @@ -177,7 +187,7 @@ def create_standard_action_set(self, player: "Player") -> ActionSet: id="leave_game", label=Localization.get(locale, "leave-table"), handler="_action_leave_game", - is_enabled="_is_leave_game_enabled", + is_enabled="_is_always_enabled", is_hidden="_is_leave_game_hidden", ) ) @@ -223,6 +233,13 @@ def setup_keybinds(self) -> None: include_spectators=True, ) # Standard keybinds + self.define_keybind( + "f1", + "Show rules", + ["show_rules"], + state=KeybindState.ALWAYS, + include_spectators=True, + ) self.define_keybind( "escape", "Actions menu", diff --git a/server/game_utils/action_visibility_mixin.py b/server/game_utils/action_visibility_mixin.py index 6692eb30..7cbefed3 100644 --- a/server/game_utils/action_visibility_mixin.py +++ b/server/game_utils/action_visibility_mixin.py @@ -109,10 +109,6 @@ def _get_toggle_spectator_label(self, player: "Player", action_id: str) -> str: return Localization.get(locale, "play") return Localization.get(locale, "spectate") - def _is_leave_game_enabled(self, player: "Player") -> str | None: - """Leave game is always enabled.""" - return None - def _is_leave_game_hidden(self, player: "Player") -> Visibility: """Leave game is always hidden (F5/keybind only).""" return Visibility.HIDDEN @@ -157,16 +153,16 @@ def _is_estimate_duration_hidden(self, player: "Player") -> Visibility: # --- Standard actions --- - def _is_show_actions_enabled(self, player: "Player") -> str | None: - """Show actions menu is always enabled.""" - return None - def _is_show_actions_hidden(self, player: "Player") -> Visibility: """Show actions is hidden for players but visible to spectators.""" if player.is_spectator: return Visibility.VISIBLE return Visibility.HIDDEN + def _is_always_enabled(self, player: "Player") -> str | None: + """Always enable an action.""" + return None + def _is_always_hidden(self, player: "Player") -> Visibility: """Always hide an action from menus (keybind only).""" return Visibility.HIDDEN diff --git a/server/game_utils/menu_management_mixin.py b/server/game_utils/menu_management_mixin.py index d0550c71..3893b9e1 100644 --- a/server/game_utils/menu_management_mixin.py +++ b/server/game_utils/menu_management_mixin.py @@ -230,3 +230,56 @@ def status_box(self, player: "Player", lines: list[str]) -> None: items=items, multiletter=False, ) + + def _action_show_rules(self, player: "Player", action_id: str) -> None: + """Show the rules document for the game.""" + user = self.get_user(player) + if not user: + return + + doc_manager = self._table.get_documents() + + folder_name = f"{self.get_type()}_rules" + meta = doc_manager.get_document_metadata(folder_name) + if not meta: + user.speak_l("documents-no-content") + return + + visible_locales = doc_manager._get_visible_locale_codes(meta, include_private=False) + if not visible_locales: + user.speak_l("documents-no-content") + return + + source_locale = meta.get("source_locale", "en") + title_locale = doc_manager._select_display_title_locale(visible_locales, user.locale, source_locale) + + if not title_locale: + user.speak_l("documents-no-content") + return + + content = doc_manager.get_document_content_for_access( + folder_name, + title_locale, + include_private=False, + ) + if content is None: + user.speak_l("documents-no-content") + return + + titles = meta.get("titles", {}) + title = doc_manager._select_visible_title( + titles, + visible_locales, + title_locale, + source_locale, + folder_name, + ) + + user.show_editbox( + "game_rules", + title, + default_value=content, + multiline=True, + read_only=True, + content_format="markdown", + ) diff --git a/server/locales/en/games.ftl b/server/locales/en/games.ftl index e23fc09f..6be6bd3e 100644 --- a/server/locales/en/games.ftl +++ b/server/locales/en/games.ftl @@ -5,6 +5,9 @@ game-name-ninetynine = Ninety Nine game-name-humanitycards = Cards Against Humanity +# Generic game actions +show-rules = Show rules + # Game categories (shared) category-party-games = Party Games diff --git a/server/locales/en/main.ftl b/server/locales/en/main.ftl index e82e4ea6..a211bed1 100644 --- a/server/locales/en/main.ftl +++ b/server/locales/en/main.ftl @@ -189,6 +189,7 @@ action-table-full = The table is full. action-no-bots = There are no bots to remove. action-bots-cannot = Bots cannot do this. action-no-scores = No scores available yet. +action-must-be-at-table = You must be at a table to view rules. # Dice actions dice-not-rolled = You haven't rolled yet.