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[];