diff --git a/apps/client/src/scenes/lobby/LobbyScene.tsx b/apps/client/src/scenes/lobby/LobbyScene.tsx
index 1bdbb14..866d055 100644
--- a/apps/client/src/scenes/lobby/LobbyScene.tsx
+++ b/apps/client/src/scenes/lobby/LobbyScene.tsx
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { domain } from "@repo/shared";
import { OVERLAY_BUTTON_STYLE } from "@client/scenes/shared/styles/overlayStyles";
+import { LobbyRuleModal } from "./components/LobbyRuleModal";
type Props = {
room: domain.room.Room | null;
@@ -45,6 +46,7 @@
const [selectedStartPlayerCount, setSelectedStartPlayerCount] = useState(
minimumStartPlayerCount,
);
+ const [isRuleModalOpen, setIsRuleModalOpen] = useState(false);
useEffect(() => {
setSelectedStartPlayerCount((prev) => {
@@ -222,22 +224,54 @@
>
ゲームスタート
+
+
) : (
- ホストの開始を待っています...
+
+ ホストの開始を待っています...
+
+
+
)}
@@ -309,6 +343,14 @@
+
+ {isRuleModalOpen && (
+ {
+ setIsRuleModalOpen(false);
+ }}
+ />
+ )}
>
);
};
diff --git a/apps/client/src/scenes/lobby/components/LobbyRuleModal.styles.ts b/apps/client/src/scenes/lobby/components/LobbyRuleModal.styles.ts
new file mode 100644
index 0000000..239cc2d
--- /dev/null
+++ b/apps/client/src/scenes/lobby/components/LobbyRuleModal.styles.ts
@@ -0,0 +1,65 @@
+import type { CSSProperties } from "react";
+import {
+ OVERLAY_PANEL_BASE_STYLE,
+ OVERLAY_PANEL_FOOTER_BASE_STYLE,
+ OVERLAY_PANEL_HEADER_BASE_STYLE,
+} from "@client/scenes/shared/styles/overlayStyles";
+
+export const LOBBY_RULE_MODAL_OVERLAY_STYLE: CSSProperties = {
+ position: "fixed",
+ inset: 0,
+ background: "rgba(0, 0, 0, 0.72)",
+ zIndex: 120,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: "16px",
+};
+
+export const LOBBY_RULE_MODAL_PANEL_STYLE: CSSProperties = {
+ ...OVERLAY_PANEL_BASE_STYLE,
+ width: "min(760px, 100%)",
+ maxHeight: "min(78dvh, 820px)",
+ display: "flex",
+ flexDirection: "column",
+};
+
+export const LOBBY_RULE_MODAL_HEADER_STYLE: CSSProperties = {
+ ...OVERLAY_PANEL_HEADER_BASE_STYLE,
+ fontSize: "1.1rem",
+ fontWeight: 800,
+};
+
+export const LOBBY_RULE_MODAL_BODY_STYLE: CSSProperties = {
+ padding: "14px 18px",
+ overflowY: "auto",
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+};
+
+export const LOBBY_RULE_MODAL_SECTION_STYLE: CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+};
+
+export const LOBBY_RULE_MODAL_SECTION_TITLE_STYLE: CSSProperties = {
+ margin: 0,
+ fontSize: "1rem",
+ fontWeight: 800,
+};
+
+export const LOBBY_RULE_MODAL_LIST_STYLE: CSSProperties = {
+ margin: 0,
+ paddingLeft: "20px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "6px",
+};
+
+export const LOBBY_RULE_MODAL_FOOTER_STYLE: CSSProperties = {
+ ...OVERLAY_PANEL_FOOTER_BASE_STYLE,
+ display: "flex",
+ justifyContent: "flex-end",
+};
diff --git a/apps/client/src/scenes/lobby/components/LobbyRuleModal.tsx b/apps/client/src/scenes/lobby/components/LobbyRuleModal.tsx
new file mode 100644
index 0000000..9613ad5
--- /dev/null
+++ b/apps/client/src/scenes/lobby/components/LobbyRuleModal.tsx
@@ -0,0 +1,34 @@
+import { OVERLAY_BUTTON_STYLE } from "@client/scenes/shared/styles/overlayStyles";
+import {
+ LOBBY_RULE_MODAL_BODY_STYLE,
+ LOBBY_RULE_MODAL_FOOTER_STYLE,
+ LOBBY_RULE_MODAL_HEADER_STYLE,
+ LOBBY_RULE_MODAL_OVERLAY_STYLE,
+ LOBBY_RULE_MODAL_PANEL_STYLE,
+} from "./LobbyRuleModal.styles";
+import { LOBBY_RULE_SECTIONS } from "../presentation/lobbyRuleContent";
+import { LobbyRuleSectionList } from "./LobbyRuleSectionList";
+
+type LobbyRuleModalProps = {
+ onClose: () => void;
+};
+
+export const LobbyRuleModal = ({ onClose }: LobbyRuleModalProps) => {
+ return (
+
+
+
ルール
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/client/src/scenes/lobby/components/LobbyRuleSectionList.tsx b/apps/client/src/scenes/lobby/components/LobbyRuleSectionList.tsx
new file mode 100644
index 0000000..588dcdc
--- /dev/null
+++ b/apps/client/src/scenes/lobby/components/LobbyRuleSectionList.tsx
@@ -0,0 +1,29 @@
+import {
+ LOBBY_RULE_MODAL_LIST_STYLE,
+ LOBBY_RULE_MODAL_SECTION_STYLE,
+ LOBBY_RULE_MODAL_SECTION_TITLE_STYLE,
+} from "./LobbyRuleModal.styles";
+import type { LobbyRuleSection } from "../presentation/lobbyRuleContent";
+
+type LobbyRuleSectionListProps = {
+ sections: LobbyRuleSection[];
+};
+
+export const LobbyRuleSectionList = ({
+ sections,
+}: LobbyRuleSectionListProps) => {
+ return (
+ <>
+ {sections.map((section) => (
+
+ {section.title}
+
+ {section.lines.map((line, index) => (
+ - {line}
+ ))}
+
+
+ ))}
+ >
+ );
+};
diff --git a/apps/client/src/scenes/lobby/presentation/lobbyRuleContent.ts b/apps/client/src/scenes/lobby/presentation/lobbyRuleContent.ts
new file mode 100644
index 0000000..b7d36c1
--- /dev/null
+++ b/apps/client/src/scenes/lobby/presentation/lobbyRuleContent.ts
@@ -0,0 +1,40 @@
+/**
+ * lobbyRuleContent
+ * ロビーのルール表示で使う短文テキストを定義する
+ */
+import { config } from "@client/config";
+
+/** ルール画面の短文セクション */
+export type LobbyRuleSection = {
+ id: "controls" | "hits" | "win";
+ title: string;
+ lines: string[];
+};
+
+/** ロビーに表示するルール内容を組み立てる */
+export const buildLobbyRuleSections = (
+ respawnHitCount: number,
+): LobbyRuleSection[] => {
+ return [
+ {
+ id: "controls",
+ title: "操作",
+ lines: ["左で移動", "右下で爆弾設置"],
+ },
+ {
+ id: "hits",
+ title: "被弾",
+ lines: ["被弾でその場ステイ", `${respawnHitCount}回で初期位置へ戻る`],
+ },
+ {
+ id: "win",
+ title: "勝利",
+ lines: ["制限時間終了時に塗り面積が最も高いチームが勝利"],
+ },
+ ];
+};
+
+/** ロビーに表示するルール内容 */
+export const LOBBY_RULE_SECTIONS: LobbyRuleSection[] = buildLobbyRuleSections(
+ config.GAME_CONFIG.PLAYER_RESPAWN_HIT_COUNT,
+);
diff --git a/apps/client/src/scenes/shared/styles/overlayStyles.ts b/apps/client/src/scenes/shared/styles/overlayStyles.ts
index f965dbf..62b46c2 100644
--- a/apps/client/src/scenes/shared/styles/overlayStyles.ts
+++ b/apps/client/src/scenes/shared/styles/overlayStyles.ts
@@ -4,6 +4,17 @@
*/
import type { CSSProperties } from "react";
+/** オーバーレイ面共通トークン */
+export const OVERLAY_SURFACE_TOKENS = {
+ PANEL_BACKGROUND: "rgba(20, 20, 20, 0.95)",
+ PANEL_BORDER: "1px solid rgba(255, 255, 255, 0.15)",
+ PANEL_SHADOW: "0 12px 28px rgba(0, 0, 0, 0.45)",
+ PANEL_RADIUS: "12px",
+ DIVIDER: "1px solid rgba(255, 255, 255, 0.16)",
+ HEADER_PADDING: "16px 18px",
+ FOOTER_PADDING: "14px 18px",
+} as const;
+
/** オーバーレイボタンの共通スタイル */
export const OVERLAY_BUTTON_STYLE: CSSProperties = {
padding: "10px 14px",
@@ -25,3 +36,24 @@
gap: "10px",
marginBottom: "10px",
};
+
+/** オーバーレイパネルの共通ベーススタイル */
+export const OVERLAY_PANEL_BASE_STYLE: CSSProperties = {
+ borderRadius: OVERLAY_SURFACE_TOKENS.PANEL_RADIUS,
+ background: OVERLAY_SURFACE_TOKENS.PANEL_BACKGROUND,
+ border: OVERLAY_SURFACE_TOKENS.PANEL_BORDER,
+ boxShadow: OVERLAY_SURFACE_TOKENS.PANEL_SHADOW,
+ color: "white",
+};
+
+/** オーバーレイパネルヘッダーの共通ベーススタイル */
+export const OVERLAY_PANEL_HEADER_BASE_STYLE: CSSProperties = {
+ padding: OVERLAY_SURFACE_TOKENS.HEADER_PADDING,
+ borderBottom: OVERLAY_SURFACE_TOKENS.DIVIDER,
+};
+
+/** オーバーレイパネルフッターの共通ベーススタイル */
+export const OVERLAY_PANEL_FOOTER_BASE_STYLE: CSSProperties = {
+ padding: OVERLAY_SURFACE_TOKENS.FOOTER_PADDING,
+ borderTop: OVERLAY_SURFACE_TOKENS.DIVIDER,
+};