From 6225403290dda5ccf2ec6e0bca4f1a61c862cc38 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Aug 2026 17:58:58 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8A=9C=ED=86=A0=EB=A6=AC=EC=96=BC:=20?= =?UTF-8?q?=EC=B4=8C=EC=9E=A5=20=EB=83=A5=EC=9D=B4=EA=B0=80=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=ED=95=98=EB=8A=94=2010=EB=8B=A8=EA=B3=84=20=EC=98=A8?= =?UTF-8?q?=EB=B3=B4=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 게임: 회관만 있는 빈 마을(고양이 0·자원 0)에서 시작 - 촌장 냥이(코숏 스프라이트) + 말풍선 가이드 오버레이 (진행도 n/10, 태스크 칩) - 라인: 인사 → 첫 입양 → 부두 건설(무료·강물 위)·배치 → 둘째 입양 → 벌목장 건설(생선 30)·배치 → 부엌 건설(목재 60) → 회관 Lv2 증축 → 작별 - 조건 충족 시 자동 진행, 인사·작별은 버튼. 완료 후 자유 운영(step 99) - 부두·벌목장을 상점 구매 건물로 이동 (기존 세이브는 "보유 중"+튜토리얼 생략) - 튜토리얼 입양 후보는 필요 적성(낚시→벌목) 보장 Co-Authored-By: Claude Fable 5 --- src/components/game/Game.tsx | 135 ++++++++++++++++++++++++++++++++++- src/lib/game/data.ts | 12 +++- src/lib/game/engine.ts | 42 +++++------ src/lib/game/types.ts | 1 + 4 files changed, 163 insertions(+), 27 deletions(-) diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index 5b5842a..e53d437 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -31,6 +31,73 @@ import type { AptitudeKey, Building, Cat, GameState } from "@/lib/game/types"; const RES_EMOJI: Record = { fish: "🐟", wood: "🪵", stone: "🪨" }; const RES_LABEL: Record = { fish: "생선", wood: "목재", stone: "돌" }; +// ---------- 튜토리얼 ---------- +interface TutStep { + text: string; + task?: string; + check?: (s: GameState) => boolean; + manual?: boolean; // 버튼으로 진행 +} + +const TUTORIAL_STEPS: TutStep[] = [ + { + text: "안녕! 난 이 마을의 촌장 냥이야. 여긴 아직 아무것도 없는 들판이지만... 우리 함께 아늑한 고양이 마을을 만들어보자!", + manual: true, + }, + { + text: "먼저 첫 친구를 데려오자. 오른쪽 위 😻 입양 버튼을 눌러서 마음에 드는 아이를 골라줘. 낚시를 잘하는 친구면 좋겠다!", + task: "고양이 1마리 입양하기", + check: (s) => s.cats.length >= 1, + }, + { + text: "이제 먹을거리를 구해야지. 🛒 상점에서 부두를 골라 강물 위에 놓아줘. 첫 건물이니까 공짜야!", + task: "부두 건설하기 (강물 위)", + check: (s) => s.buildings.some((b) => b.type === "dock"), + }, + { + text: "뚝딱뚝딱... 부두가 완성되면 부두를 눌러서 친구를 배치해줘. 생선이 모이면 배지를 눌러 거둬들이는 것도 잊지 마!", + task: "부두에 고양이 배치하기", + check: (s) => { + const d = s.buildings.find((b) => b.type === "dock"); + return !!d && d.constructionUntil === undefined && d.assigned.length >= 1; + }, + }, + { + text: "생선이 모이는 동안 친구를 한 마리 더 데려오자! 이번엔 벌목을 잘하는 아이면 좋겠어.", + task: "고양이 2마리째 입양하기", + check: (s) => s.cats.length >= 2, + }, + { + text: "생선 30마리면 벌목장을 지을 수 있어. 목재는 모든 건설의 기본이란다!", + task: "벌목장 건설하기 (🐟 30)", + check: (s) => s.buildings.some((b) => b.type === "lumberyard"), + }, + { + text: "벌목장이 완성되면 벌목 잘하는 친구를 배치해줘!", + task: "벌목장에 고양이 배치하기", + check: (s) => { + const l = s.buildings.find((b) => b.type === "lumberyard"); + return !!l && l.constructionUntil === undefined && l.assigned.length >= 1; + }, + }, + { + text: "일하다 보면 배가 고파져. 목재 60을 모아 부엌을 짓자. 요리 잘하는 친구가 있으면 배치해두면 알아서 밥을 챙겨 먹는단다.", + task: "부엌 건설하기 (🪵 60)", + check: (s) => s.buildings.some((b) => b.type === "kitchen"), + }, + { + text: "마지막이야! 회관을 눌러 Lv2로 증축하면 친구를 더 데려올 수 있어. 열심히 일한 친구는 레벨이 오르니까, 목재 100·생선 80을 모으면서 기다려보자.", + task: "마을 회관 Lv2 증축 (🪵100 · 🐟80 · Lv3 친구 1마리)", + check: (s) => s.hall.level >= 2, + }, + { + text: "축하해! 이제 이 마을은 온전히 네 거야. 건물을 늘리고, 마을을 꾸미고, 친구들을 돌봐줘. 난 회관에 있을게 — 언제든 놀러 와! 🐾", + manual: true, + }, +]; + +const TUTORIAL_DONE = 99; + type Panel = | { type: "building"; id: string } | { type: "hall" } @@ -59,6 +126,17 @@ export default function Game() { if (msgTimer.current) clearTimeout(msgTimer.current); }, []); + // 튜토리얼 자동 진행 (조건 충족 시) + useEffect(() => { + if (!state) return; + const step = state.tutorialStep ?? TUTORIAL_DONE; + if (step >= TUTORIAL_STEPS.length) return; + const def = TUTORIAL_STEPS[step]; + if (def.check && def.check(state)) { + dispatch(() => engine.setTutorialStep(step + 1)); + } + }, [state, dispatch]); + // 오프라인 정산 알림 (1회) const offlineShown = useRef(false); useEffect(() => { @@ -182,7 +260,18 @@ export default function Game() { 🐾 고양이 + )} + + + ); + })()} + {/* ── DB 상태 (좌하단 미니) ── */}
@@ -472,7 +601,9 @@ function ShopPanel({
{def.name}
- {costEntries.map(([k, v]) => `${RES_EMOJI[k]} ${v}`).join(" + ")} + {costEntries.length > 0 + ? costEntries.map(([k, v]) => `${RES_EMOJI[k]} ${v}`).join(" + ") + : "무료"} {` · ⏱ ${def.buildTime}초`} {def.reqHallLv > 1 && ` · 회관 Lv${def.reqHallLv}`}
diff --git a/src/lib/game/data.ts b/src/lib/game/data.ts index bcaf457..a52b4a2 100644 --- a/src/lib/game/data.ts +++ b/src/lib/game/data.ts @@ -149,7 +149,7 @@ export const APT_POINT_EVERY = 3; // 3레벨마다 적성 포인트 1 export interface BuildDef { id: string; // 건물 인스턴스 id (단일 건설) - type: "quarry" | "kitchen" | "cat_house" | "sun_deck"; + type: "dock" | "lumberyard" | "quarry" | "kitchen" | "cat_house" | "sun_deck"; kind?: "work" | "passive"; // passive: 고양이 배치 없음 (기본 work) name: string; emoji: string; @@ -163,6 +163,16 @@ export interface BuildDef { } export const BUILD_DEFS: BuildDef[] = [ + { + id: "dock_1", type: "dock", name: "부두", emoji: "⚓", + aptitude: "fishing", resource: "fish", maxSlots: 3, cost: {}, reqHallLv: 1, buildTime: 15, + desc: "낚시 적성 고양이가 생선을 잡습니다. 강물 위에 지어요. 첫 건물이라 무료!", + }, + { + id: "lumberyard_1", type: "lumberyard", name: "벌목장", emoji: "🪓", + aptitude: "logging", resource: "wood", maxSlots: 3, cost: { fish: 30 }, reqHallLv: 1, buildTime: 30, + desc: "벌목 적성 고양이가 목재를 모읍니다. 목재는 모든 건설의 기본!", + }, { id: "kitchen_1", type: "kitchen", name: "부엌", emoji: "🍳", aptitude: "cooking", maxSlots: 2, cost: { wood: 60 }, reqHallLv: 1, buildTime: 60, diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts index cb75179..d8395dc 100644 --- a/src/lib/game/engine.ts +++ b/src/lib/game/engine.ts @@ -106,30 +106,16 @@ function generateCat(forceAptitude?: AptitudeKey): Cat { // ---------- 초기 상태 ---------- -function createBuildings(): Building[] { - return [ - { - id: "dock_1", type: "dock", name: "부두", emoji: "⚓", - pos: { ...DEFAULT_BUILDING_POS.dock_1 }, - level: 1, maxSlots: 3, aptitude: "fishing", resource: "fish", - assigned: [], stored: 0, - }, - { - id: "lumberyard_1", type: "lumberyard", name: "벌목장", emoji: "🪓", - pos: { ...DEFAULT_BUILDING_POS.lumberyard_1 }, - level: 1, maxSlots: 3, aptitude: "logging", resource: "wood", - assigned: [], stored: 0, - }, - ]; -} - function createInitialState(): GameState { - // 시작 고양이 2마리는 각각 낚시·벌목이 가능하도록 보정 (초반 막힘 방지) + // 새 게임: 회관만 있는 빈 마을에서 촌장 냥이 튜토리얼로 시작 return { version: 1, + boardVersion: BOARD_VERSION, + tutorialStep: 0, resources: { fish: 0, wood: 0, stone: 0 }, - cats: [generateCat("fishing"), generateCat("logging")], - buildings: createBuildings(), + cats: [], + buildings: [], + decors: [], hall: { level: 1, capacity: 3 }, updatedAt: Date.now(), }; @@ -291,6 +277,7 @@ export const engine = { for (const b of state.buildings) b.pos ??= { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 5 }) }; state.decors ??= []; + state.tutorialStep ??= 99; // 기존 마을은 튜토리얼 생략 // 보드 개편(크기·풋프린트 변경) 시 건물 위치를 새 기본값으로 재배치 if (state.boardVersion !== BOARD_VERSION) { for (const b of state.buildings) @@ -317,6 +304,12 @@ export const engine = { return r; }, + /** 튜토리얼 진행 갱신 */ + setTutorialStep(step: number) { + if (!state) return; + state.tutorialStep = step; + }, + tick(dt: number) { if (!state) return; for (const building of state.buildings) { @@ -489,9 +482,9 @@ export const engine = { state.updatedAt = Date.now(); }, - /** 입양 후보 생성 (아직 마을에 합류하지 않음) */ - generateCandidates(count = 3): Cat[] { - return Array.from({ length: count }, () => generateCat()); + /** 입양 후보 생성 (아직 마을에 합류하지 않음). forceApt로 특정 적성 보장 */ + generateCandidates(count = 3, forceApt?: AptitudeKey): Cat[] { + return Array.from({ length: count }, () => generateCat(forceApt)); }, /** 후보 중 한 마리 입양 확정 */ @@ -505,7 +498,7 @@ export const engine = { /** 공통 건설: 상점 구매 후 선택한 타일에 배치 (비용은 배치 확정 시 차감) */ build( - type: "quarry" | "kitchen" | "cat_house" | "sun_deck", + type: "dock" | "lumberyard" | "quarry" | "kitchen" | "cat_house" | "sun_deck", pos: { i: number; j: number }, ): string | null { if (!state) return null; @@ -563,6 +556,7 @@ export const engine = { .join(", ")})`; consume(state, def.cost); state.decors ??= []; + state.tutorialStep ??= 99; // 기존 마을은 튜토리얼 생략 // 같은 타일의 기존 장식 제거 후 배치 state.decors = state.decors.filter( (d) => !(d.pos.i === pos.i && d.pos.j === pos.j), diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 19fa0dd..284887d 100644 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -68,6 +68,7 @@ export interface DecoTile { export interface GameState { version: 1; boardVersion?: number; // 보드 개편 마이그레이션 마커 + tutorialStep?: number; // 튜토리얼 진행 (99 = 완료) decors?: DecoTile[]; // 꾸미기 바닥 타일 resources: Record; cats: Cat[];