Phase 2~4 그레이박스 시스템 완성 및 내러티브 태그 재설계
1. Phase 2 완료 — 자원 경제 & 덱빌딩
- 파산 문책 시스템: 토큰 마이너스 상태로 결재 시 파산 카운터 증가,
의회에서 단계별 경고 → 미개선 시 다음 의회에서 해임 게임오버
- 오염 카드 등급 확률: 엔트로피/내부저항/대중공황/위험계수 단계에 비례한
S/A/B/C 가중치로 의회 종료 시 오염 카드 자동 주입
(신화/조직저항/사회공황/이사회압박 16종 카드 신규 추가)
- 자산 효과 4종 구현: 성질 변환·시한부 격리·물리적 영구 소각·결재권 강제 행사
(자산 슬롯 클릭으로 발동)
2. Phase 3 완료 — 정치 시스템 & 스토리 페이징
- 글로벌 지부장 12개 파벌이 각자 다른 공식으로 우호도를 계산하는 엔진 추가.
게임 시작 시 5명 무작위 선발, 동맹/파국 성향 임원은 국장의 개입(매수/감사)에 면역
- 내러티브 태그 누적치에 따라 페이즈1(일상/위기) → 페이즈2(작전/극비) 카드 풀이
실시간으로 전환되도록 구현
- 버그 수정: 위험 수용 계수(risk)가 직접 게임오버를 발동시키지만 EndingScreen에
대응 엔딩이 없어 "미확인 종말" 빈 폴백으로 빠지던 문제 (risk는 탄핵 기준치로만 사용하도록 수정)
- 버그 수정: CouncilScreen의 임원 초기화 effect가 매수 액션의 risk 상승으로
재실행되며 방금 강제 전환한 투표 결과를 덮어쓰던 문제 (마운트 시 1회만 초기화하도록 수정)
3. Phase 4 완료 — 멀티 엔딩 & 연대기
- 우선순위 기반 엔딩 판별기(endings.js) 신규: 의회 정치 엔딩 > 내러티브 연쇄 엔딩 >
변주된 붕괴 엔딩(자원 태그 3종 + 토큰 3종) > 기본 붕괴 엔딩, 총 15종 엔딩 카탈로그
- 극단적 파라미터 조기 엔딩: 단일 결재로 파라미터가 급격히 5단계에 도달하면
"돌이킬 수 없는 순간" 즉시 발동
- 엔딩 도감(EndingCodex) 신규 컴포넌트: 해금/미해금(???) 표시,
localStorage(ss_endings_v2)에 엔딩 고유 id 기준으로 영구 저장
- 연대기 팝업에 타임라인(세로선 + 턴별 점) 시각화 추가
4. 내러티브 태그 시스템 재설계
- 기존에는 결재 카드 한 장당 무조건 +1 되던 카운터였으나, "항상 얻는 보상이 아니다"라는
기획 의도에 맞춰 선별된 카드(22/63장)의 좌/우 선택지에만 짧은 키워드를 부여하도록 변경
(cards.csv에 left_narrative/right_narrative 컬럼 추가)
- 연대기 팝업 상단과 엔딩 화면에 수집한 키워드를 칩(chip) 형태로 표시
- 위 재설계로 태그 획득이 희소해진 만큼, 페이즈 전환 문턱을 4/8 → 2/4로 하향 조정
5. 기타
- 로컬 Node.js LTS 설치 및 Playwright 기반 실기 플레이 검증 체계 확립
- ROADMAP.md/CLAUDE.md Phase 상태 갱신 (Phase 0~4 그레이박스 구간 완료,
다음은 외부 플레이어 그레이박스 테스트)
This commit is contained in:
119
client/src/data/endings.js
Normal file
119
client/src/data/endings.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// 멀티 엔딩 판별기 (Phase 4)
|
||||
// 우선순위: 의회 정치 엔딩 > 내러티브 연쇄 엔딩 > 변주된 붕괴 엔딩 > 기본 붕괴 엔딩
|
||||
|
||||
// 의회 수장 성향 축 — 상임 이사회 8대 이념(§4.1) 중 기존 자원 태그로 매핑 가능한 3개 축
|
||||
export const CHAIRMAN_AXES = ['force', 'wealth', 'surveillance'];
|
||||
|
||||
// 내러티브 연쇄 엔딩: 4종 오염 카드가 이 순서로(연속일 필요는 없음) 한 번씩 등장해야 완성된다
|
||||
const NARRATIVE_CHAIN_SEQUENCE = ['신화', '조직저항', '사회공황', '이사회압박'];
|
||||
|
||||
export const isNarrativeChainComplete = (sequence) => {
|
||||
let i = 0;
|
||||
for (const type of sequence) {
|
||||
if (type === NARRATIVE_CHAIN_SEQUENCE[i]) i++;
|
||||
if (i === NARRATIVE_CHAIN_SEQUENCE.length) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const PARAM_LABEL = { resistance: '내부 저항', entropy: '엔트로피', panic: '대중 공황' };
|
||||
|
||||
export const ENDING_CATALOG = [
|
||||
{ id: 'bankruptcy', title: '파산 문책 (조용한 해임)',
|
||||
desc: '텅 빈 금고와 산더미 같은 미결 청구서 앞에서 이사회는 더 이상 웃지 않았습니다. 세상이 끝나기도 전에, 당신의 자리가 먼저 끝났습니다.',
|
||||
reason: '연속된 재정 파탄 경고를 개선하지 못함' },
|
||||
|
||||
{ id: 'council_victory', title: '의회 정치 엔딩 (그림자 뒤의 승리)',
|
||||
desc: '탄핵안은 가결되었지만, 이사회 의장은 이미 당신이 쌓아온 힘 앞에 고개를 숙였습니다. 해임장에 서명하는 손이 떨리고 있었습니다. 자리는 잃었지만, 진짜 권력은 잃지 않았습니다.',
|
||||
reason: '탄핵 가결 + 의회 의장 성향 조건 충족' },
|
||||
|
||||
{ id: 'narrative_chain', title: '내러티브 연쇄 엔딩 (진실의 연쇄)',
|
||||
desc: '신화, 조직의 균열, 대중의 광기, 권력의 사슬 — 당신은 이 모든 사건이 하나의 손에서 비롯되었다는 진실을 끝내 마주했습니다. 아무도 믿지 않겠지만, 적어도 당신만은 알고 있습니다.',
|
||||
reason: '신화 → 조직저항 → 사회공황 → 이사회압박 순서로 내러티브 사건 수집' },
|
||||
|
||||
{ id: 'impeachment', title: '정치적 탄핵 (윗선 붕괴)',
|
||||
desc: '그림자 위원회가 당신의 권한을 박탈했습니다. 당신의 모든 업적은 지워질 것입니다.',
|
||||
reason: '주간 의회에서 적대적 파벌 임원 9명 이상 찬성' },
|
||||
|
||||
{ id: 'resistance_base', title: '사임 (내부 붕괴)',
|
||||
desc: '조직원들의 불만이 폭발하여 폭동이 일어났습니다. 당신은 자리에서 끌려 내려왔습니다.',
|
||||
reason: '내부 저항 파라미터 5단계 도달' },
|
||||
{ id: 'resistance_coup', title: '군사 쿠데타 (내부 붕괴)',
|
||||
desc: '무력을 손에 쥔 자들은 더 이상 결재를 기다리지 않았습니다. 총구가 당신의 사무실을 겨눴습니다.',
|
||||
reason: '내부 저항 5단계 + 무력 태그 다량 보유' },
|
||||
{ id: 'resistance_flee', title: '야반도주 (내부 붕괴)',
|
||||
desc: '텅 빈 급여 계좌를 확인한 요원들이 하나둘 사라졌습니다. 마지막까지 남은 건 당신과 꺼진 사무실 불빛뿐이었습니다.',
|
||||
reason: '내부 저항 5단계 + 인력 토큰 심각한 마이너스' },
|
||||
|
||||
{ id: 'entropy_base', title: '세계 멸망 (외부 붕괴)',
|
||||
desc: '장막이 찢어지고 이계의 존재들이 쏟아져 들어옵니다. 우리가 알던 세계는 끝났습니다.',
|
||||
reason: '엔트로피 파라미터 5단계 도달' },
|
||||
{ id: 'entropy_coverup', title: '은폐된 멸망 (외부 붕괴)',
|
||||
desc: '언론과 감시망을 총동원해 진실을 덮었지만, 덮은 것은 진실이 아니라 탈출할 시간이었습니다. 세계는 아무도 모르게 끝났습니다.',
|
||||
reason: '엔트로피 5단계 + 감시 태그 다량 보유' },
|
||||
{ id: 'entropy_blackout', title: '정보 암흑 (외부 붕괴)',
|
||||
desc: '정보망이 완전히 말라붙은 순간, 당신은 세계가 무너지는 소리조차 듣지 못했습니다.',
|
||||
reason: '엔트로피 5단계 + 정보 토큰 심각한 마이너스' },
|
||||
|
||||
{ id: 'panic_base', title: '문명 붕괴 (사회 붕괴)',
|
||||
desc: '대중은 광기에 휩싸였고, 문명은 스스로를 파괴했습니다. 통제 불능의 시대가 도래했습니다.',
|
||||
reason: '대중 공황 파라미터 5단계 도달' },
|
||||
{ id: 'panic_capitalism', title: '자본의 폭주 (사회 붕괴)',
|
||||
desc: '재난 앞에서도 시장은 웃었습니다. 부가 쌓일수록 거리는 비어갔고, 마지막엔 돈을 셀 사람조차 남지 않았습니다.',
|
||||
reason: '대중 공황 5단계 + 재력 태그 다량 보유' },
|
||||
{ id: 'panic_riot', title: '긴축의 폭동 (사회 붕괴)',
|
||||
desc: '텅 빈 금고를 본 대중은 더 이상 설명을 기다리지 않았습니다.',
|
||||
reason: '대중 공황 5단계 + 예산 토큰 심각한 마이너스' },
|
||||
|
||||
{ id: 'sudden_collapse', title: '돌이킬 수 없는 순간 (즉각 붕괴)',
|
||||
desc: '망설임 없는 결정 한 번이 모든 단계를 건너뛰었습니다. 경고도, 예고도 없이 — 그저 끝났습니다.',
|
||||
reason: '단일 결정으로 파라미터가 급격히 5단계에 도달' },
|
||||
|
||||
{ id: 'unidentified', title: '미확인 종말',
|
||||
desc: '기록되지 않은 파멸입니다.',
|
||||
reason: '알 수 없는 원인' },
|
||||
];
|
||||
|
||||
const byId = Object.fromEntries(ENDING_CATALOG.map((e) => [e.id, e]));
|
||||
|
||||
export const getEndingById = (id) => byId[id] || byId.unidentified;
|
||||
|
||||
const VARIANT_MAP = {
|
||||
resistance: [
|
||||
{ id: 'resistance_coup', condition: (c) => c.tags.force >= 3 },
|
||||
{ id: 'resistance_flee', condition: (c) => c.tokens.personnel <= -50 },
|
||||
],
|
||||
entropy: [
|
||||
{ id: 'entropy_coverup', condition: (c) => c.tags.surveillance >= 3 },
|
||||
{ id: 'entropy_blackout', condition: (c) => c.tokens.info <= -50 },
|
||||
],
|
||||
panic: [
|
||||
{ id: 'panic_capitalism', condition: (c) => c.tags.wealth >= 3 },
|
||||
{ id: 'panic_riot', condition: (c) => c.tokens.budget <= -50 },
|
||||
],
|
||||
};
|
||||
|
||||
const BASE_MAP = { resistance: 'resistance_base', entropy: 'entropy_base', panic: 'panic_base' };
|
||||
|
||||
// ctx: { trigger: 'impeachment' | 'collapse', params, tags, tokens, lastDelta, chairmanAxis, narrativeChainUnlocked }
|
||||
export function resolveEnding(ctx) {
|
||||
if (ctx.trigger === 'impeachment') {
|
||||
if (ctx.chairmanAxis && ctx.tags[ctx.chairmanAxis] >= 5) return byId.council_victory;
|
||||
if (ctx.narrativeChainUnlocked) return byId.narrative_chain;
|
||||
return byId.impeachment;
|
||||
}
|
||||
|
||||
if (ctx.narrativeChainUnlocked) return byId.narrative_chain;
|
||||
|
||||
const param = ['resistance', 'entropy', 'panic'].find((p) => ctx.params[p] >= 5);
|
||||
if (!param) return byId.unidentified;
|
||||
|
||||
if (ctx.lastDelta && ctx.lastDelta.param === param && ctx.lastDelta.amount >= 3) {
|
||||
return { ...byId.sudden_collapse, reason: `${PARAM_LABEL[param]} 단일 결정으로 급격히 5단계 도달` };
|
||||
}
|
||||
|
||||
const variant = (VARIANT_MAP[param] || []).find((v) => v.condition(ctx));
|
||||
if (variant) return byId[variant.id];
|
||||
|
||||
return byId[BASE_MAP[param]];
|
||||
}
|
||||
Reference in New Issue
Block a user