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}

+ +
+ ))} + + ); +}; 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, +};