Files
ProjectSS/client/src/components/CouncilScreen.jsx
Kyoung5seo 56f494f331 피드백 수정 사항 적용 (모두 반영된 것은 아님)
▶주간 의회 명칭 변경: 주간 의회 --> 정기 의회
▶라운드 당 결재 건수는 다름 : 라운드가 증가할때마다 결재 건수가 줄어들도록 하여 아쉬움 발생 - 결재 건수가 0이 되면 게임 오버 (검토를 해보고 줄어드는게 나을지 늘어나는게 나을지 검토 해줘)
▶라운드 구성 개념의 디테일 부족: 기획서에 플로우차트(다이어그램)를 추가 ('결재 단계'와 '주간 의회 단계'의 흐름이 플레이어와 개발자 모두에게 명확하도록)
▶확인 결과 아래 데이터 없음. 데이터 추가 필요 (추가한 데이터가 시스템에 맞게 작동하도록 구현 해줘)
- 자산 데이터
- 오염 카드 데이터
- 내러티브 데이터
▶토큰 마이너스 상태 처리: 방향 B (패널티 및 파산 - 블랙코미디에 어울림), 마이너스를 허용하되 가혹한 대가를 치르게 하기
파산 문책 시스템 기획 및 구현 필요 (아래는 간략 시스템 작성 - 블랙 코미디가 구현되면 좋겠음)
- 마이너스 상태에서 결재 1건 처리 할 때 마다, 파산 카운터 증가
- 정기 의회에서 파산 문책 : 파산 카운터에 따라 문책의 강도가 달라 짐
- 파산 문책으로 경고를 받았는데도 개선(마이너스를 0 이상으로 복구)하지 못하면 다음 정기 의회에서 게임 오버
▶글이 너무 길어 읽기 힘든 문제:
- 게임 플레이 필수 글 (직관성 최우선): 카드 상단에 상황 키워드를 배치하고, 그 아래에 질문을 배치해 무엇을 요구 하는지 확실하게 알려 줌 (예: 상황 키워드 = 요원들의 시위 발생, 질문 = 시위를 어떻게 하시겠습니까?) 그리고 좌우 스와이프 시 등장하는 보상 표시에서 left_text, right_text의 텍스트 크기를 키우고 그 아래의 보상 오브젝트(예:  [예산 -2] ,  [지지도 +10] )는 텍스트보다는 기호와 숫자를 활용하여 1초 만에 파악할 수 있도록 함.
- 서사에 필요한 글 (메인 텍스트): '게임 플레이 필수 글'에서 추가적인 스토리 정보를 원한다면 봐야 하는 글로 카드 데이터에서 text컬럼의 내용을 표시 합니다. 게임 플레이 필수 글보다는 폰트가 작아야 하고, 너무 길지 않게 핵심 서사 위주로 설명 합니다. (예를 들면, 상황 키워드가 요원들의 시위 발생인데, 왜 시위가 발생했는지에 대한 서사를 알려 주면 됩니다)
- 블랙 코미디/플레이버 텍스트 (선택적 정보): 공간을 분리합니다.(공간 구분이 확실해야 합니다.) 카드 하단에 글자를 작게 하고, 폰트를 다르게 하여 구분되도록 합니다.
2026-06-24 23:08:32 +09:00

215 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import './CouncilScreen.css';
export default function CouncilScreen({ params, setParams, tokens, setTokens, tags, setTags, bankruptcyCount, setBankruptcyCount, bankruptcyWarning, onResolve }) {
const [members, setMembers] = useState([]);
const [agenda, setAgenda] = useState('');
const [impeachment, setImpeachment] = useState(false);
const [actionTaken, setActionTaken] = useState(false);
const [result, setResult] = useState(null);
const [isBankruptcyWarning, setIsBankruptcyWarning] = useState(false);
const [bankruptPenalty, setBankruptPenalty] = useState('');
useEffect(() => {
const isNegative = tokens.budget < 0 || tokens.personnel < 0 || tokens.info < 0;
if (isNegative) {
if (bankruptcyWarning) {
setAgenda('재무 건전성 악화 및 횡령 배임 혐의');
setResult('당신은 파산했고 자금 횡령으로 체포되었습니다. 감옥의 식사는 다행히 무료입니다.');
return;
} else {
setIsBankruptcyWarning(true);
setAgenda('파산 문책: 재무 경고');
if (bankruptcyCount >= 5) {
setBankruptPenalty(`파산 카운터 ${bankruptcyCount}회: 의회가 매우 분노했습니다. 모든 위험도 파라미터가 상승합니다.`);
setParams(prev => ({
resistance: Math.min(5, prev.resistance + 1),
entropy: Math.min(5, prev.entropy + 1),
panic: Math.min(5, prev.panic + 1),
risk: Math.min(5, prev.risk + 1)
}));
} else {
setBankruptPenalty(`파산 카운터 ${bankruptcyCount}회: 의회에서 강력한 경고가 나왔습니다. 다음 의회까지 복구하지 못하면 해임됩니다.`);
}
}
} else {
// Normal Agenda
const newMembers = Array.from({ length: 13 }).map((_, i) => {
const type = i < 8 ? 'board' : 'global';
const rand = Math.random() * 100;
let vote = '미정';
if (rand < params.risk * 20) {
vote = '반대';
} else if (rand > 50) {
vote = '찬성';
}
return { id: i, type, vote };
});
setMembers(newMembers);
const againstCount = newMembers.filter(m => m.vote === '반대').length;
if (againstCount >= 9) {
setAgenda('정치적 탄핵안 상정');
setImpeachment(true);
} else {
const randAgenda = Math.random() > 0.5 ? '이득 규약 법안 상정' : '표적 규약 법안 상정';
setAgenda(randAgenda);
}
}
}, [params.risk, tokens, bankruptcyWarning, bankruptcyCount]);
const handleAction = (type) => {
if (actionTaken) return;
const newMembers = [...members];
if (type === 'bribe' && tokens.budget >= 30) {
setTokens(prev => ({ ...prev, budget: prev.budget - 30 }));
setParams(prev => ({ ...prev, risk: Math.min(5, prev.risk + 1) }));
// Force 2 against to support
let changed = 0;
for (let m of newMembers) {
if (m.vote === '반대' && changed < 2) {
m.vote = '찬성';
changed++;
}
}
} else if (type === 'manipulate' && tokens.info >= 30) {
setTokens(prev => ({ ...prev, info: prev.info - 30 }));
setParams(prev => ({ ...prev, entropy: Math.min(5, prev.entropy + 1) }));
// Delay agenda
setResult('법안 상정 이월 (장부 조작 성공)');
setActionTaken(true);
return;
} else if (type === 'audit' && tokens.personnel >= 30) {
setTokens(prev => ({ ...prev, personnel: prev.personnel - 30 }));
setParams(prev => ({ ...prev, resistance: Math.min(5, prev.resistance + 1) }));
// Force 3 against to abstain
let changed = 0;
for (let m of newMembers) {
if (m.vote === '반대' && changed < 3) {
m.vote = '미정';
changed++;
}
}
} else {
return; // Not enough tokens
}
setMembers(newMembers);
setActionTaken(true);
};
const finalize = () => {
if (result && result.includes('파산')) {
onResolve({ bankrupted: true });
return;
}
if (result) {
onResolve({ impeachment: false });
return;
}
if (isBankruptcyWarning) {
// Acknowledged warning
onResolve({ impeachment: false, bankruptcyWarning: true });
return;
}
const againstCount = members.filter(m => m.vote === '반대').length;
if (impeachment) {
if (againstCount >= 9) {
onResolve({ impeachment: true });
} else {
onResolve({ impeachment: false });
}
} else {
const supportCount = members.filter(m => m.vote === '찬성').length;
if (supportCount > againstCount) {
if (agenda.includes('이득')) {
setTags(prev => ({ ...prev, wealth: prev.wealth + 1 }));
} else {
setTags(prev => ({ ...prev, wealth: Math.max(0, prev.wealth - 1) }));
}
}
// If tokens are positive, clear bankruptcy warning
onResolve({ impeachment: false, bankruptcyWarning: false });
}
};
return (
<div className="council-screen">
<div className="council-header">
<h2>[📜 이번 정기 상정: {agenda}]</h2>
<div className="risk-bar-container">
<span>위험 수용 계수:</span>
<div className="risk-bar">
<div className="risk-fill" style={{ width: `${(params.risk / 5) * 100}%` }}></div>
</div>
</div>
{impeachment && <div className="warning-blink">[🚨 정치적 탄핵안 상정!]</div>}
</div>
{isBankruptcyWarning ? (
<div className="bankruptcy-warning-box">
<p className="penalty-text">{bankruptPenalty}</p>
</div>
) : (
<div className="members-grid">
{members.map(m => (
<div key={m.id} className={`member-icon ${m.type}`}>
<span className="member-avatar">👤</span>
<span className="member-badge">
{m.vote === '찬성' ? '✅' : m.vote === '반대' ? '❌' : '❓'}
</span>
</div>
))}
</div>
)}
{result ? (
<div className="council-result">
<h3>결과: {result}</h3>
<button className="fat-btn" onClick={finalize}>의회 종료</button>
</div>
) : isBankruptcyWarning ? (
<div className="council-actions">
<button className="fat-btn action-proceed" onClick={finalize}>경고 수용 의회 종료</button>
</div>
) : (
<div className="council-actions">
<button
className={`fat-btn ${tokens.budget < 30 ? 'disabled' : ''}`}
onClick={() => handleAction('bribe')}
disabled={actionTaken || tokens.budget < 30}
>
💰 매수 (예산 30 소모 / 임원 찬성)
</button>
<button
className={`fat-btn ${tokens.info < 30 ? 'disabled' : ''}`}
onClick={() => handleAction('manipulate')}
disabled={actionTaken || tokens.info < 30}
>
👁 장부 조작 (정보 30 소모 / 안건 이월)
</button>
<button
className={`fat-btn ${tokens.personnel < 30 ? 'disabled' : ''}`}
onClick={() => handleAction('audit')}
disabled={actionTaken || tokens.personnel < 30}
>
🩸 내부 감사 (인력 30 소모 / 기권 유도)
</button>
{actionTaken && (
<button className="fat-btn action-proceed" onClick={finalize}>표결 진행</button>
)}
{!actionTaken && (
<button className="fat-btn action-proceed" onClick={finalize}>개입 없이 표결 진행</button>
)}
</div>
)}
</div>
);
}