구현 우선 순위 1차 진행 완료
- 1차 — 코어 검증 (여기까지만 되어도 재미 판정 가능) 1. **기호 체계 + 책상 상태 상시 노출 UI** (§3) 2. **책상 적재 + 누진 패시브 페널티 A·B** (§4) 3. **표준 족보 판정 및 합본 실행** (§5) 4. **합본 기반 토큰 산출** (§8.2) 게임 플레이 후 피드백 해결 - 내용은 아래 문서 참고 - docs/decisions/2026-09-05_core-graybox-ux-feedback.md
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import Papa from 'papaparse';
|
||||
import cardsCsvRaw from '../../data/cards.csv?raw';
|
||||
import DeskBar from './DeskBar';
|
||||
import CoreSwipeCard from './CoreSwipeCard';
|
||||
import CombinePanel from './CombinePanel';
|
||||
import HandGuidePopup from './HandGuidePopup';
|
||||
import { pickPollutionEntry } from '../engine/pollutionPool';
|
||||
import { makeDeskCard, applyPassiveGaugeRise, addToDesk, removeFromDesk } from '../engine/desk';
|
||||
import './CoreGameScreen.css';
|
||||
|
||||
// 코어 루프 그레이박스 (§2·§17 1차) — 기호 체계 + 책상 적재/누진 페널티(A·B) + 표준 족보 판정/합본 +
|
||||
// 합본 기반 토큰 산출까지만 구현한다. 상점·규약·의회·자산·전례·엔딩은 2~4차 범위라 아직 없다.
|
||||
// 기존 GameScreen.jsx/engine/pollution.js는 참조용으로만 남기고 건드리지 않는다 — 코어시스템
|
||||
// 기획서 자체가 "기존 코드는 참조용, 신규 개발" 원칙을 명시하고 있다.
|
||||
|
||||
const buildCard = (row) => ({
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
keyword: row.keyword,
|
||||
question: row.question,
|
||||
text: row.text,
|
||||
flavor: row.flavor,
|
||||
left: {
|
||||
text: row.left_text,
|
||||
axis: row.left_axis || null,
|
||||
grade: row.left_grade ? Number(row.left_grade) : null,
|
||||
tag: row.left_tag || null,
|
||||
},
|
||||
right: {
|
||||
text: row.right_text,
|
||||
axis: row.right_axis || null,
|
||||
grade: row.right_grade ? Number(row.right_grade) : null,
|
||||
tag: row.right_tag || null,
|
||||
},
|
||||
});
|
||||
|
||||
const INITIAL_GAUGES = { entropy: 0, resistance: 0, panic: 0, risk: 0 };
|
||||
const INITIAL_TOKENS = { budget: 0, personnel: 0, info: 0 };
|
||||
|
||||
export default function CoreGameScreen() {
|
||||
const deck = useMemo(() => {
|
||||
const parsed = Papa.parse(cardsCsvRaw, { header: true, skipEmptyLines: true });
|
||||
return parsed.data.map(buildCard);
|
||||
}, []);
|
||||
|
||||
const [cardIndex, setCardIndex] = useState(0);
|
||||
const [deskCards, setDeskCards] = useState([]);
|
||||
const [gauges, setGauges] = useState(INITIAL_GAUGES);
|
||||
const [tokens, setTokens] = useState(INITIAL_TOKENS);
|
||||
const [score, setScore] = useState(0);
|
||||
const [justifications, setJustifications] = useState([]);
|
||||
const [showCombine, setShowCombine] = useState(false);
|
||||
const [showGuide, setShowGuide] = useState(false);
|
||||
const [log, setLog] = useState([]);
|
||||
|
||||
const pushLog = (msg) => setLog((prev) => [msg, ...prev].slice(0, 6));
|
||||
|
||||
const currentCard = deck.length > 0 ? deck[cardIndex % deck.length] : null;
|
||||
|
||||
const handleSwipe = (direction) => {
|
||||
if (!currentCard) return;
|
||||
const side = direction === 'right' ? currentCard.right : currentCard.left;
|
||||
|
||||
// A. 게이지 상시 상승 — 이번 결재 시점 책상 상태 기준으로 축별 누진율만큼 올린다 (§4.2-A).
|
||||
setGauges((prev) => applyPassiveGaugeRise(prev, deskCards));
|
||||
|
||||
// 선택한 쪽에 축이 지정돼 있으면 오염 카드 1장을 책상에 적재한다.
|
||||
// pollution_pool.csv에서 실제 이름·사연을 붙여서, "이 선택이 어떤 서류를 남겼는지"가
|
||||
// 축 라벨이 아니라 구체적인 사건으로 보이게 한다 (플레이테스트 피드백).
|
||||
if (side.axis) {
|
||||
const grade = side.grade || 1;
|
||||
const entry = pickPollutionEntry(side.axis, grade);
|
||||
const newCard = makeDeskCard(side.axis, grade, entry || {});
|
||||
setDeskCards((prev) => addToDesk(prev, [newCard]));
|
||||
pushLog(`『${side.text}』 결재됨 → 오염 서류 『${entry?.name || '미상'}』 접수`);
|
||||
} else {
|
||||
pushLog(`『${side.text}』 결재됨 — 오염 없음`);
|
||||
}
|
||||
|
||||
setCardIndex((i) => (i + 1) % deck.length);
|
||||
};
|
||||
|
||||
const handleCombineConfirm = (result) => {
|
||||
setDeskCards((prev) => removeFromDesk(prev, result.removeIds));
|
||||
setTokens((prev) => {
|
||||
const next = { ...prev };
|
||||
Object.entries(result.tokens).forEach(([k, v]) => { next[k] = (next[k] || 0) + v; });
|
||||
return next;
|
||||
});
|
||||
setJustifications((prev) => [...prev, result.justification]);
|
||||
setScore((prev) => prev + result.performanceScore);
|
||||
pushLog(`합본 성립: 『${result.pattern.name}』 — 실적 +${result.performanceScore}, 명분(${result.justification.type}) +${result.justification.amount}`);
|
||||
setShowCombine(false);
|
||||
};
|
||||
|
||||
if (!currentCard) {
|
||||
return <div className="core-loading">카드를 불러오는 중...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="core-game-screen">
|
||||
<DeskBar
|
||||
deskCards={deskCards}
|
||||
gauges={gauges}
|
||||
onOpenCombine={() => setShowCombine(true)}
|
||||
onOpenGuide={() => setShowGuide(true)}
|
||||
/>
|
||||
|
||||
<div className="core-status-row">
|
||||
<span>실적 {score}</span>
|
||||
<span>예산 {tokens.budget} · 인력 {tokens.personnel} · 정보 {tokens.info}</span>
|
||||
<span>명분 {justifications.length}개 보유</span>
|
||||
</div>
|
||||
|
||||
<CoreSwipeCard card={currentCard} onSwipe={handleSwipe} />
|
||||
|
||||
<div className="core-log">
|
||||
{log.map((line, i) => <div key={i} className="core-log-line">{line}</div>)}
|
||||
</div>
|
||||
|
||||
{showCombine && (
|
||||
<CombinePanel
|
||||
deskCards={deskCards}
|
||||
onConfirm={handleCombineConfirm}
|
||||
onClose={() => setShowCombine(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showGuide && <HandGuidePopup onClose={() => setShowGuide(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user