Merge branch 'main' of https://git.mint-grape.duckdns.org/kyoung5/ProjectSS
This commit is contained in:
11
CLAUDE.md
11
CLAUDE.md
@@ -74,9 +74,14 @@ Phase 5 [ ] 비주얼 폴리싱 & UX ← 그레이박스 테스트 완
|
||||
|
||||
**Phase 4 완료 시 남은 주의사항:** 엔딩 도감 localStorage 키를 `ss_endings_v2`로 교체했다
|
||||
(기존 `ss_endings`는 제목 문자열 기반이라 취약했음). 이전 플레이테스트 기록은 새 키와
|
||||
호환되지 않는다. 내러티브 태그는 모든 카드가 아니라 선별된 카드(22/63장)만 키워드를 주는
|
||||
방식으로 재설계했고, 페이즈 전환 문턱도 4/8 → 2/4로 낮췄다. 자세한 내용은 `ROADMAP.md`
|
||||
Phase 4 참고.
|
||||
호환되지 않는다. 내러티브 태그는 재사용 가능한 태그 풀(`narrative_tags.json`, 10종) 방식으로,
|
||||
선별된 카드의 좌/우 선택지에서만 얻는다. 페이즈 전환 문턱은 "고유 태그 3종/6종 획득". 자세한
|
||||
내용은 `ROADMAP.md` Phase 4 참고.
|
||||
|
||||
**⚠️ 2026-07-09 병렬 브랜치 병합:** 이 폴더가 유일한 작업 사본이 아니라면, 작업 시작 전
|
||||
`git fetch` 후 `git log origin/main..HEAD`와 `git log HEAD..origin/main`으로 분기 여부를
|
||||
반드시 확인할 것. 과거에 다른 세션이 origin에 푸시한 커밋을 이 로컬이 받지 못한 채 Phase 2~4를
|
||||
독립적으로 재구현해 대형 충돌이 났던 적이 있다. 자세한 경위는 `ROADMAP.md` 상단 박스 참고.
|
||||
|
||||
Phase 0~4 그레이박스 구간이 모두 완료됐다. 다음은 외부 플레이어 그레이박스 테스트이며,
|
||||
Phase 5(비주얼 폴리싱)는 그 피드백 반영 후에만 착수한다.
|
||||
|
||||
53
ROADMAP.md
53
ROADMAP.md
@@ -42,6 +42,19 @@ Phase 5 [ ] 비주얼 폴리싱 & UX ← 그레이박스 테스트 완
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 2026-07-09: 병렬 작업 브랜치 병합 (중요 — 반드시 읽을 것)
|
||||
|
||||
이 로컬 작업 폴더는 2026-06-24~25에 **다른 세션에서 origin/main에 푸시된 4개 커밋**을
|
||||
받아오지 못한 채로 Phase 2~4 작업을 진행했다. 그 커밋들에는 이 로컬에서 독립적으로
|
||||
다시 구현한 것과 겹치는 기능(파산 문책, 내러티브 태그, 오염 카드, 자산)이 서로 다른
|
||||
방식으로 이미 존재했고, `git pull` 시점에 6개 파일에서 충돌이 나서 origin 쪽 구현을
|
||||
기준으로 병합했다. 원인·과정·잔여 이슈는 아래 각 Phase 절의 "병합 참고" 박스에 정리했다.
|
||||
|
||||
**이 폴더가 유일한 작업 사본이 아니라면, 다음 세션을 시작하기 전에 반드시 `git fetch` +
|
||||
`git log origin/main..HEAD` / `git log HEAD..origin/main`으로 분기 여부를 먼저 확인할 것.**
|
||||
|
||||
---
|
||||
|
||||
## 다음 단계
|
||||
|
||||
**Phase 0~4 그레이박스 작업이 모두 완료됐다.** 이제 외부 플레이어 그레이박스 테스트를 진행할 차례다.
|
||||
@@ -108,7 +121,14 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한
|
||||
> **참고:** 오염 카드 콘텐츠(`cards.csv`의 신화/조직저항/사회공황/이사회압박 16종)는 등급별 1종씩만 채운 **초안**이다.
|
||||
> 밸런스·문체 검토 후 디자이너가 등급별로 추가 카드를 늘리는 작업이 별도로 필요하다.
|
||||
> 또한 A급 이상 신화 카드가 '뉴스 이벤트' 카드를 추가하는 연쇄 규칙(기획서 92-95행)은 이번 범위에 포함하지 않았다.
|
||||
> node/npm이 설치되지 않은 환경이라 `npm run dev` 실기 검증은 못 했고, 코드 리뷰로만 확인했다 — 실행 확인 필요.
|
||||
|
||||
> **병합 참고 (2026-07-09):** origin/main에 미리 푸시되어 있던 다른 세션의 파산 문책 구현(정례회의
|
||||
> 라운드가 늘수록 결재 건수가 8→6→4→2→0으로 줄어들고, 파산 카운터 5회 이상이면 의회에서 전 파라미터를
|
||||
> 강제로 올리는 에스컬레이션 포함)이 더 완성도 높고 실제 사용자 피드백을 직접 반영한 것이었다. 그 버전을
|
||||
> 기준으로 병합하면서 상태명을 `bankruptcyCounter/bankruptcyWarned` → `bankruptcyCount/bankruptcyWarning`으로
|
||||
> 통일했다. 자산은 origin에 `assets.json`(이름 있는 개별 아이템 4종: 전임 국장의 만년필 등)이 준비돼 있었지만
|
||||
> 실제 효과 로직이 연결돼 있지 않아서, 이 로컬에서 만든 "성질 변환/시한부 격리/물리적 영구 소각/결재권 강제
|
||||
> 행사" 4종 기능 로직을 그대로 유지했다 — `assets.json`의 이름 있는 아이템 플레이버는 아직 미사용 상태다.
|
||||
|
||||
### 핵심 참조 문서
|
||||
- `시스템 기획서` — 결재 단계, 덱 오염, 덱 압축, 자산 슬롯
|
||||
@@ -130,18 +150,20 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한
|
||||
- [x] 연대기(Chronicle) 팝업
|
||||
- [x] **파벌 우호도 세부 조건** — 12개 글로벌 지부장 파벌이 각자 다른 공식으로 우호도를 계산한다(`GameScreen.jsx` `GLOBAL_FACTIONS`). 게임 시작 시 12개 중 5개를 무작위 선발(`factionRoster`), 매 의회마다 실시간 상태(토큰/태그/파라미터/자산/카운터)로 점수를 산출해 동맹/우호/중립/적대/파국 5단계 티어로 환산, 투표와 국장 개입 면역 여부를 결정한다(`CouncilScreen.jsx`).
|
||||
- [x] **파산 카운터 → 의회 문책 연동** — Phase 2 작업으로 완료됨 (`CouncilScreen.jsx`가 `bankruptcyCounter`/`bankruptcyWarned`를 직접 참조)
|
||||
- [x] **스토리 페이징 카드 풀 전환** — 초기 덱은 일상/위기만 포함되고, 내러티브 태그 4개 도달 시 작전/극비 프로젝트 카드 풀이 실시간으로 덱에 개방된다(`GameScreen.jsx` `phase2Pool`/`phase2Unlocked`).
|
||||
- [x] **스토리 페이징 카드 풀 전환** — 초기 덱은 일상/위기만 포함되고, 고유 내러티브 태그 3종 도달 시 작전/극비 프로젝트 카드 풀이 실시간으로 덱에 개방된다(`GameScreen.jsx` `phase2Pool`/`phase2Unlocked`).
|
||||
|
||||
### 완료 기준 (Done Criteria)
|
||||
- [x] 글로벌 지부장 12개 파벌의 우호도가 각자 다른 조건으로 개별 계산된다.
|
||||
- [x] 파산 카운터가 의회 문책 로직에 연동된다.
|
||||
- [x] 내러티브 태그 4개 도달 시 등장 카드 풀이 실시간으로 전환된다.
|
||||
- [x] 내러티브 태그 누적 시 등장 카드 풀이 실시간으로 전환된다.
|
||||
|
||||
> **참고 및 남은 과제:**
|
||||
> - 상임 이사회 8명의 투표는 여전히 `params.risk` 기반 확률 처리다. 기획서상 이들은 국장의 '성향 태그'에 반응해야 하는데, 이 게임에는 아직 성향 태그 시스템 자체가 없어 범위 밖으로 남겨뒀다(§4.1 대응).
|
||||
> - 태그 8개(페이즈3) 시점의 카드 풀은 기획서상 페이즈2와 동일한 유형(작전/극비/오염)이라 별도 전환 이벤트를 추가하지 않았다. 대신 오염 카드 등급 가중치가 파라미터 상승에 따라 자연스럽게 심해지는 방식으로 "압박 루프" 강도를 구현했다.
|
||||
> - Playwright로 실제 브라우저에서 플레이 검증 중 **`params.risk >= 5`가 직접 게임오버를 발동시키지만 `EndingScreen`에 해당 분기가 없어 "미확인 종말" 폴백으로 빠지는 버그**를 발견해 수정했다(PRD상 risk는 탄핵 기준치일 뿐 직접 붕괴 조건이 아님).
|
||||
|
||||
> **병합 참고 (2026-07-09):** 내러티브 태그 시스템 자체를 origin 버전(재사용 가능한 태그 풀 방식, 아래 참고)으로
|
||||
> 교체하면서 페이징 문턱도 "고유 태그 4개/8개" → "고유 태그 3개/6개"로 재조정했다(풀 크기 10종 기준).
|
||||
|
||||
### 핵심 참조 문서
|
||||
- `스토리 바이블` — §4.2 글로벌 지부장 12개 파벌 상세
|
||||
- `시스템 기획서` — 주간 의회 단계, 토큰 경제
|
||||
@@ -169,8 +191,9 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한
|
||||
- [x] **변주된 붕괴 엔딩** — 자원 태그 변주 3종(쿠데타/은폐된 멸망/자본의 폭주) + 토큰 변주 3종(야반도주/정보 암흑/긴축의 폭동)
|
||||
- [x] **극단적 파라미터 조기 엔딩** — 단일 결재에서 위험 파라미터가 3단계 이상 급증하며 5단계에 도달하면 "돌이킬 수 없는 순간" 즉각 붕괴 엔딩 발동
|
||||
- [x] **내러티브 연쇄 엔딩** — 오염 카드 4종을 지정 순서로 수집하면 해금, 이후 탄핵/붕괴 시 최우선 적용
|
||||
- [x] **도감 UI** — `EndingCodex.jsx` 신규 컴포넌트. 엔딩 화면에서 "📖 엔딩 도감 보기"로 열람, 해금 15종 중 몇 종 확인했는지 표시, 미해금은 "???"로 잠금 처리, `localStorage`(`ss_endings_v2`)에 엔딩 id 기준으로 영구 저장
|
||||
- [x] **연대기 팝업 완성** — 좌측 세로선 + 턴별 점(dot)으로 타임라인 형태 시각화(`ChroniclePopup.css`)
|
||||
- [x] **임기 만료 엔딩** (병합으로 추가) — 라운드가 거듭될수록 정례회의까지의 결재 건수가 8→6→4→2→0으로 줄어들다 0이 되면 발동
|
||||
- [x] **도감 UI** — `EndingCodex.jsx` 신규 컴포넌트. 엔딩 화면에서 "📖 엔딩 도감 보기"로 열람, 해금 16종 중 몇 종 확인했는지 표시, 미해금은 "???"로 잠금 처리, `localStorage`(`ss_endings_v2`)에 엔딩 id 기준으로 영구 저장
|
||||
- [x] **연대기 팝업 완성** — 턴별 타임라인 + 새로 획득한 내러티브 태그를 아이콘/설명과 함께 인라인으로 강조 표시, 상단에 수집한 태그 전체를 칩으로 요약(`ChroniclePopup.jsx/css`)
|
||||
|
||||
### 완료 기준 (Done Criteria)
|
||||
- [x] 파라미터 5단계 도달 시 우선순위 판별기가 정확한 엔딩을 출력한다. (Playwright로 "군사 쿠데타" 변주 엔딩과 "정치적 탄핵" 기본 엔딩을 각각 실제로 트리거해 확인)
|
||||
@@ -179,16 +202,18 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한
|
||||
- [x] 연대기 팝업에서 이번 회차 내러티브 히스토리를 확인할 수 있다.
|
||||
|
||||
> **참고:** 엔딩 도감 localStorage 키를 `ss_endings`(제목 문자열 기반, 취약)에서 `ss_endings_v2`(고유 id 기반)로 교체했다.
|
||||
> 이전에 수동 플레이테스트로 쌓인 `ss_endings` 데이터는 새 키와 호환되지 않아 브라우저에서 도감이 0/15로 보일 수 있다 —
|
||||
> 이전에 수동 플레이테스트로 쌓인 `ss_endings` 데이터는 새 키와 호환되지 않아 브라우저에서 도감이 0/16으로 보일 수 있다 —
|
||||
> 프로토타입 단계라 마이그레이션 로직은 만들지 않았다.
|
||||
|
||||
> **내러티브 태그 시스템 재설계 (디자이너 피드백 반영):** 최초 구현은 결재 카드 한 장당 무조건 +1 되는
|
||||
> 카운터였는데, 이는 "내러티브 태그는 항상 얻는 것이 아니다"라는 기획 의도와 맞지 않았다. `cards.csv`에
|
||||
> `left_narrative` / `right_narrative` 컬럼을 추가해 선별된 카드(위기/작전/극비 22종 + 오염 카드 A·S등급
|
||||
> 8종, 총 22장/63장)의 좌/우 선택지에만 짧은 키워드(예: "은폐 공작 실행", "쿠데타 조짐")를 부여하도록
|
||||
> 바꿨다. 수집한 키워드는 연대기 팝업 상단과 엔딩 화면에서 칩(chip) 형태로 확인할 수 있다(기존 턴별
|
||||
> 결재 로그는 그대로 유지). 동시에 페이즈 전환 문턱을 4/8 → 2/4로 낮췄다(키워드가 희소해진 만큼, 그리고
|
||||
> 기존 4/8도 실제 플레이에서 길게 느껴진다는 피드백 반영).
|
||||
> **병합 참고 (2026-07-09) — 내러티브 태그 시스템 최종본:** 이 Phase에서 한 번(카드당 자유 텍스트 키워드
|
||||
> `left_narrative`/`right_narrative`) 재설계했었는데, 이후 origin/main에 병합할 다른 세션의 구현을 발견했다.
|
||||
> 그 버전은 `narrative_tags.json`(재사용 가능한 태그 10종 — 아이콘+이름+설명, 예: "쿠데타의 그림자" 🗡️)과
|
||||
> `cards.csv`의 `left_tag`/`right_tag` 컬럼(태그 id 참조) 조합으로, 처음 획득하는 태그일 때만 토스트 알림이
|
||||
> 뜨고 진행도에 반영되는 방식이었다 — 사용자 피드백을 더 직접 반영한 더 성숙한 설계라 최종적으로 이 방식을
|
||||
> 채택하고 자유 텍스트 방식은 폐기했다. 다만 origin의 `cards.csv`에는 태그 컬럼 자체가 비어 있어 실제로는
|
||||
> 작동하지 않는 상태였으므로, `update_csv_tags.js`의 기존 11개 매핑을 참고해 새로 채워 넣고 태그 풀도
|
||||
> 4종(쿠데타의 그림자/군중의 광기/정치적 압박/희생의 대가) 추가했다. 페이징 문턱도 "태그 풀 10종 중 고유
|
||||
> 3종/6종 획득"으로 재조정했다.
|
||||
|
||||
> **이 Phase 완료 = 그레이박스 테스트 준비 완료.**
|
||||
> Phase 5는 테스트 피드백 반영이 끝난 후 착수한다.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -14,14 +14,14 @@
|
||||
.chronicle-popup {
|
||||
background: var(--color-surface);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
max-height: 80%;
|
||||
max-width: 500px;
|
||||
max-height: 85%;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 10px 50px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.chronicle-header {
|
||||
@@ -29,14 +29,23 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.chronicle-header h2 {
|
||||
margin: 0;
|
||||
font-family: var(--font-doc-title);
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.chronicle-tags-summary {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@@ -45,6 +54,7 @@
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.keyword-section {
|
||||
@@ -85,7 +95,7 @@
|
||||
}
|
||||
|
||||
.chronicle-content {
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -96,50 +106,106 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chronicle-list {
|
||||
list-style: none;
|
||||
padding: 0 0 0 14px;
|
||||
margin: 0;
|
||||
border-left: 2px solid rgba(255, 255, 255, 0.1);
|
||||
.chronicle-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chronicle-item {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
.chronicle-entry {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 16px;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.chronicle-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -21px;
|
||||
top: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-token-info);
|
||||
box-shadow: 0 0 0 3px var(--color-surface);
|
||||
}
|
||||
|
||||
.chronicle-item:last-child {
|
||||
.chronicle-entry:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.turn-number {
|
||||
font-size: 12px;
|
||||
color: var(--color-token-info);
|
||||
.entry-turn {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #a1a1aa;
|
||||
min-width: 60px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.chronicle-item p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #ddd;
|
||||
.entry-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.entry-context {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entry-type {
|
||||
font-size: 12px;
|
||||
color: var(--color-token-info);
|
||||
font-family: var(--font-typewriter);
|
||||
}
|
||||
|
||||
.entry-keyword {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.entry-text {
|
||||
font-size: 14px;
|
||||
color: #d4d4d8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.entry-decision {
|
||||
font-size: 14px;
|
||||
color: #4ade80;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.entry-tag-highlight {
|
||||
margin-top: 12px;
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
border-left: 4px solid #fbbf24;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.tag-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tag-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.tag-desc {
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.has-tag {
|
||||
background: linear-gradient(90deg, rgba(251,191,36,0.05) 0%, transparent 100%);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border-bottom: none;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import React from 'react';
|
||||
import './ChroniclePopup.css';
|
||||
|
||||
export default function ChroniclePopup({ chronicle, keywords = [], onClose }) {
|
||||
export default function ChroniclePopup({ chronicle, acquiredTags = [], onClose }) {
|
||||
return (
|
||||
<div className="chronicle-overlay" onClick={onClose}>
|
||||
<div className="chronicle-popup" onClick={e => e.stopPropagation()}>
|
||||
<div className="chronicle-header">
|
||||
<h2>국장의 연대기</h2>
|
||||
<div className="chronicle-tags-summary">
|
||||
획득한 내러티브: {acquiredTags.length}개
|
||||
</div>
|
||||
<button className="close-btn" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="keyword-section">
|
||||
<span className="keyword-label">수집한 키워드</span>
|
||||
{keywords.length === 0 ? (
|
||||
{acquiredTags.length === 0 ? (
|
||||
<span className="keyword-empty">아직 수집한 키워드가 없습니다.</span>
|
||||
) : (
|
||||
<div className="keyword-chips">
|
||||
{keywords.map((k, idx) => (
|
||||
<span key={idx} className="keyword-chip" title={`Turn ${k.turn}`}>{k.word}</span>
|
||||
{acquiredTags.map((t) => (
|
||||
<span key={t.id} className="keyword-chip" title={t.description}>{t.icon} {t.name}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -27,14 +30,31 @@ export default function ChroniclePopup({ chronicle, keywords = [], onClose }) {
|
||||
{chronicle.length === 0 ? (
|
||||
<p className="empty-text">아직 기록된 역사가 없습니다.</p>
|
||||
) : (
|
||||
<ul className="chronicle-list">
|
||||
<div className="chronicle-timeline">
|
||||
{chronicle.map((entry, idx) => (
|
||||
<li key={idx} className="chronicle-item">
|
||||
<span className="turn-number">Turn {entry.turn}</span>
|
||||
<p>{entry.text}</p>
|
||||
</li>
|
||||
<div key={idx} className={`chronicle-entry ${entry.tag ? 'has-tag' : ''}`}>
|
||||
<div className="entry-turn">Turn {entry.turn}</div>
|
||||
<div className="entry-body">
|
||||
<div className="entry-context">
|
||||
<span className="entry-type">[{entry.cardType}]</span>
|
||||
{entry.keyword && <span className="entry-keyword">{entry.keyword}</span>}
|
||||
</div>
|
||||
<div className="entry-text">{entry.text}</div>
|
||||
{entry.decision && <div className="entry-decision">👉 결정: {entry.decision}</div>}
|
||||
|
||||
{entry.tag && (
|
||||
<div className="entry-tag-highlight">
|
||||
<div className="tag-icon">{entry.tag.icon}</div>
|
||||
<div className="tag-details">
|
||||
<div className="tag-name">내러티브 획득: {entry.tag.name}</div>
|
||||
<div className="tag-desc">{entry.tag.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,32 +149,19 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bankruptcy-warning {
|
||||
margin-top: 12px;
|
||||
.bankruptcy-warning-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.penalty-text {
|
||||
color: var(--color-risk-critical);
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.council-screen.bankruptcy-termination {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.council-screen.bankruptcy-termination h2 {
|
||||
font-family: var(--font-doc-title);
|
||||
color: var(--color-risk-critical);
|
||||
}
|
||||
|
||||
.bankruptcy-quote {
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.council-screen.bankruptcy-termination .fat-btn {
|
||||
max-width: 280px;
|
||||
margin-top: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './CouncilScreen.css';
|
||||
|
||||
const getBankruptcyTier = (counter) => {
|
||||
if (counter >= 8) return { label: '최종 경고', text: '이사회 최종 경고: 다음에도 장부가 마이너스라면, 당신의 자리는 없습니다.' };
|
||||
if (counter >= 4) return { label: '공식 문책', text: '인사위원회가 소환장을 보냈습니다. 법인카드는 잠시 압수하겠습니다.' };
|
||||
return { label: '가벼운 경고', text: '회계팀이 슬쩍 눈치를 주기 시작했습니다. 영수증 좀 잘 챙기시죠.' };
|
||||
};
|
||||
|
||||
export default function CouncilScreen({ params, setParams, tokens, setTokens, tags, setTags, bankruptcyCounter, bankruptcyWarned, globalFactions = [], onBribe, onResolve }) {
|
||||
export default function CouncilScreen({ params, setParams, tokens, setTokens, tags, setTags, bankruptcyCount, bankruptcyWarning, globalFactions = [], onBribe, 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 isNegative = tokens.budget < 0 || tokens.personnel < 0 || tokens.info < 0;
|
||||
const bankruptcyTermination = isNegative && bankruptcyWarned;
|
||||
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;
|
||||
}
|
||||
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}회: 의회에서 강력한 경고가 나왔습니다. 다음 의회까지 복구하지 못하면 해임됩니다.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 상임 이사회 8명: 위험 수용 계수 기반 무작위 (성향 태그 세부 연동은 Phase 3 범위 밖)
|
||||
const boardMembers = Array.from({ length: 8 }).map((_, i) => {
|
||||
const rand = Math.random() * 100;
|
||||
@@ -43,15 +61,15 @@ export default function CouncilScreen({ params, setParams, tokens, setTokens, ta
|
||||
const randAgenda = Math.random() > 0.5 ? '이득 규약 법안 상정' : '표적 규약 법안 상정';
|
||||
setAgenda(randAgenda);
|
||||
}
|
||||
// 의회는 마운트 시 1회만 초기화한다 (params.risk를 deps에 넣으면 매수 액션의 risk 상승이
|
||||
// 이 effect를 재실행시켜 방금 강제 전환한 투표를 덮어써버리는 버그가 있었다)
|
||||
// 의회는 마운트 시 1회만 초기화한다 (deps에 tokens/params.risk를 넣으면 매수 액션 등으로
|
||||
// 값이 바뀔 때마다 재실행되어 방금 강제 전환한 투표 결과를 덮어써버리는 버그가 있었다)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
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) }));
|
||||
@@ -85,23 +103,32 @@ export default function CouncilScreen({ params, setParams, tokens, setTokens, ta
|
||||
} else {
|
||||
return; // Not enough tokens
|
||||
}
|
||||
|
||||
|
||||
setMembers(newMembers);
|
||||
setActionTaken(true);
|
||||
};
|
||||
|
||||
const finalize = () => {
|
||||
if (result && result.includes('파산')) {
|
||||
onResolve({ bankrupted: true });
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
onResolve({ impeachment: false, bankruptcy: false, bankruptcyWarning: isNegative });
|
||||
onResolve({ impeachment: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBankruptcyWarning) {
|
||||
onResolve({ impeachment: false, bankruptcyWarning: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const againstCount = members.filter(m => m.vote === '반대').length;
|
||||
if (impeachment) {
|
||||
if (againstCount >= 9) {
|
||||
onResolve({ impeachment: true, bankruptcy: false, bankruptcyWarning: isNegative });
|
||||
onResolve({ impeachment: true });
|
||||
} else {
|
||||
onResolve({ impeachment: false, bankruptcy: false, bankruptcyWarning: isNegative });
|
||||
onResolve({ impeachment: false, bankruptcyWarning: false });
|
||||
}
|
||||
} else {
|
||||
const supportCount = members.filter(m => m.vote === '찬성').length;
|
||||
@@ -112,27 +139,15 @@ export default function CouncilScreen({ params, setParams, tokens, setTokens, ta
|
||||
setTags(prev => ({ ...prev, wealth: Math.max(0, prev.wealth - 1) }));
|
||||
}
|
||||
}
|
||||
onResolve({ impeachment: false, bankruptcy: false, bankruptcyWarning: isNegative });
|
||||
// 토큰이 정상 범위로 돌아왔다면 파산 경고를 해제한다
|
||||
onResolve({ impeachment: false, bankruptcyWarning: false });
|
||||
}
|
||||
};
|
||||
|
||||
if (bankruptcyTermination) {
|
||||
return (
|
||||
<div className="council-screen bankruptcy-termination">
|
||||
<h2>🚨 재정 파탄 특별 청문회</h2>
|
||||
<p>지난 경고에도 불구하고 장부는 여전히 마이너스입니다. 이사회는 더 이상 변명을 듣지 않기로 했습니다.</p>
|
||||
<p className="bankruptcy-quote">"숫자는 거짓말을 하지 않습니다. 당신의 자리도 이제 안녕입니다."</p>
|
||||
<button className="fat-btn" onClick={() => onResolve({ impeachment: false, bankruptcy: true, bankruptcyWarning: true })}>해임 통보 수령</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tier = getBankruptcyTier(bankruptcyCounter);
|
||||
|
||||
return (
|
||||
<div className="council-screen">
|
||||
<div className="council-header">
|
||||
<h2>[📜 이번 주 상정: {agenda}]</h2>
|
||||
<h2>[📜 이번 정기 상정: {agenda}]</h2>
|
||||
<div className="risk-bar-container">
|
||||
<span>위험 수용 계수:</span>
|
||||
<div className="risk-bar">
|
||||
@@ -140,51 +155,58 @@ export default function CouncilScreen({ params, setParams, tokens, setTokens, ta
|
||||
</div>
|
||||
</div>
|
||||
{impeachment && <div className="warning-blink">[🚨 정치적 탄핵안 상정!]</div>}
|
||||
{isNegative && !bankruptcyWarned && (
|
||||
<div className="bankruptcy-warning">⚠️ [{tier.label}] {tier.text}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="members-grid">
|
||||
{members.map(m => (
|
||||
<div key={m.id} className={`member-icon ${m.type}`} title={m.label}>
|
||||
<span className="member-avatar">👤</span>
|
||||
<span className="member-badge">
|
||||
{m.vote === '찬성' ? '✅' : m.vote === '반대' ? '❌' : '❓'}
|
||||
</span>
|
||||
</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}`} title={m.label}>
|
||||
<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' : ''}`}
|
||||
<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' : ''}`}
|
||||
<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' : ''}`}
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.round-info {
|
||||
font-family: var(--font-typewriter);
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.parameters {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
@@ -227,3 +234,43 @@
|
||||
background: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
.tag-notification {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border: 1px solid #fbbf24;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
box-shadow: 0 4px 20px rgba(251, 191, 36, 0.4);
|
||||
z-index: 100;
|
||||
animation: slideDownFadeOut 3s forwards;
|
||||
}
|
||||
|
||||
.tag-noti-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.tag-noti-info h4 {
|
||||
margin: 0;
|
||||
color: #fbbf24;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tag-noti-info p {
|
||||
margin: 4px 0 0 0;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@keyframes slideDownFadeOut {
|
||||
0% { opacity: 0; transform: translate(-50%, -20px); }
|
||||
10% { opacity: 1; transform: translate(-50%, 0); }
|
||||
80% { opacity: 1; transform: translate(-50%, 0); }
|
||||
100% { opacity: 0; transform: translate(-50%, -20px); }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Papa from 'papaparse';
|
||||
import cardsCsvRaw from '../data/cards.csv?raw';
|
||||
import narrativeTagsData from '../data/narrative_tags.json';
|
||||
import { BookOpen, HandCoins, Droplet, Eye, Shield, Building2, EyeOff } from 'lucide-react';
|
||||
import SwipeCard from './SwipeCard';
|
||||
import CouncilScreen from './CouncilScreen';
|
||||
@@ -38,9 +39,10 @@ const pickGrade = (level) => {
|
||||
return 'C';
|
||||
};
|
||||
|
||||
// 내러티브 키워드 누적치에 따른 페이즈 전환 문턱 (기존 4/8은 실제 플레이에서 너무 길게 느껴져 하향 조정)
|
||||
const PHASE2_TAG_THRESHOLD = 2;
|
||||
const PHASE3_TAG_THRESHOLD = 4;
|
||||
// 내러티브 태그는 narrative_tags.json 풀(10종)에서 "처음 획득하는 태그"만 진행도에 반영된다.
|
||||
// 문턱은 풀 크기 대비 낮게 잡는다 (기존 4/8은 실제 플레이에서 길게 느껴진다는 피드백 반영).
|
||||
const PHASE2_TAG_THRESHOLD = 3;
|
||||
const PHASE3_TAG_THRESHOLD = 6;
|
||||
|
||||
const ASSET_META = {
|
||||
transform: { icon: '🧪', label: '성질 변환' },
|
||||
@@ -58,7 +60,7 @@ const GLOBAL_FACTIONS = [
|
||||
{ id: 'spindoctor', name: '여론 통제국', computeAffinity: (c) => c.tags.surveillance * 18 - 30 },
|
||||
{ id: 'union', name: '현장 요원 노조', computeAffinity: (c) => (2 - c.params.resistance) * 30 },
|
||||
{ id: 'risk', name: '리스크 관리반', computeAffinity: (c) => (2 - c.params.risk) * 30 },
|
||||
{ id: 'occult', name: '오컬트 수집가 협회', computeAffinity: (c) => c.mythicCount * 35 + c.narrativeTags * 3 - 20 },
|
||||
{ id: 'occult', name: '오컬트 수집가 협회', computeAffinity: (c) => c.mythicCount * 35 + c.tagCount * 3 - 20 },
|
||||
{ id: 'blackops', name: '극비 프로젝트 연구소', computeAffinity: (c) => c.assetsAcquiredCount * 25 - 30 },
|
||||
{ id: 'scavenger', name: '잔해 청소부 길드', computeAffinity: (c) => c.purgeCount * 35 - 20 },
|
||||
{ id: 'entropy', name: '엔트로피 관측소', computeAffinity: (c) => (2 - c.params.entropy) * 30 },
|
||||
@@ -78,13 +80,15 @@ const TIER_IMMUNE = new Set(['동맹', '파국']);
|
||||
const buildCardFromRow = (row) => ({
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
keyword: row.keyword,
|
||||
question: row.question,
|
||||
text: row.text,
|
||||
flavor: row.flavor,
|
||||
grade: row.grade || null,
|
||||
contaminated: CONTAM_TYPES.has(row.type),
|
||||
left: {
|
||||
text: row.left_text,
|
||||
narrativeTag: row.left_narrative || null,
|
||||
tag: row.left_tag ? narrativeTagsData.find(t => t.id === row.left_tag) : null,
|
||||
stats: {
|
||||
res: row.left_res || 0, ent: row.left_ent || 0, pan: row.left_pan || 0, rsk: row.left_rsk || 0,
|
||||
bud: row.left_bud || 0, per: row.left_per || 0, inf: row.left_inf || 0,
|
||||
@@ -93,7 +97,7 @@ const buildCardFromRow = (row) => ({
|
||||
},
|
||||
right: {
|
||||
text: row.right_text,
|
||||
narrativeTag: row.right_narrative || null,
|
||||
tag: row.right_tag ? narrativeTagsData.find(t => t.id === row.right_tag) : null,
|
||||
stats: {
|
||||
res: row.right_res || 0, ent: row.right_ent || 0, pan: row.right_pan || 0, rsk: row.right_rsk || 0,
|
||||
bud: row.right_bud || 0, per: row.right_per || 0, inf: row.right_inf || 0,
|
||||
@@ -117,8 +121,8 @@ export function GameScreen() {
|
||||
const [params, setParams] = useState({ resistance: 0, entropy: 0, panic: 0, risk: 0 });
|
||||
const [tokens, setTokens] = useState({ budget: 50, personnel: 50, info: 50 });
|
||||
const [tags, setTags] = useState({ wealth: 0, force: 0, surveillance: 0 });
|
||||
const [narrativeTags, setNarrativeTags] = useState(0);
|
||||
const [keywords, setKeywords] = useState([]);
|
||||
const [acquiredTags, setAcquiredTags] = useState([]);
|
||||
const [newTagNotification, setNewTagNotification] = useState(null);
|
||||
const [phase, setPhase] = useState(1);
|
||||
const [gameOver, setGameOver] = useState(false);
|
||||
const [cards, setCards] = useState([]);
|
||||
@@ -133,9 +137,13 @@ export function GameScreen() {
|
||||
const [showChronicle, setShowChronicle] = useState(false);
|
||||
const [assets, setAssets] = useState([]);
|
||||
|
||||
// 라운드 구조: 라운드가 증가할수록 정례회의까지 남은 결재 건수가 줄어든다 (시간 압박 시각화)
|
||||
const [roundCount, setRoundCount] = useState(1);
|
||||
const [approvalsRemaining, setApprovalsRemaining] = useState(10);
|
||||
|
||||
// 파산 문책 시스템
|
||||
const [bankruptcyCounter, setBankruptcyCounter] = useState(0);
|
||||
const [bankruptcyWarned, setBankruptcyWarned] = useState(false);
|
||||
const [bankruptcyCount, setBankruptcyCount] = useState(0);
|
||||
const [bankruptcyWarning, setBankruptcyWarning] = useState(false);
|
||||
|
||||
// 자산 효과 상태
|
||||
const [mulliganShields, setMulliganShields] = useState(0);
|
||||
@@ -179,9 +187,9 @@ export function GameScreen() {
|
||||
setChairmanAxis(CHAIRMAN_AXES[Math.floor(Math.random() * CHAIRMAN_AXES.length)]);
|
||||
}, []);
|
||||
|
||||
// 내러티브 키워드가 PHASE2_TAG_THRESHOLD개 도달 시 페이즈2 카드 풀(작전/극비) 개방 — 스토리 페이징 카드 풀 전환
|
||||
// 내러티브 태그가 PHASE2_TAG_THRESHOLD종 도달 시 페이즈2 카드 풀(작전/극비) 개방
|
||||
useEffect(() => {
|
||||
if (phase2Unlocked || phase2Pool.length === 0 || narrativeTags < PHASE2_TAG_THRESHOLD) return;
|
||||
if (phase2Unlocked || phase2Pool.length === 0 || acquiredTags.length < PHASE2_TAG_THRESHOLD) return;
|
||||
setPhase2Unlocked(true);
|
||||
setCards(prev => {
|
||||
const shuffledPool = [...phase2Pool].sort(() => Math.random() - 0.5);
|
||||
@@ -190,12 +198,12 @@ export function GameScreen() {
|
||||
next.splice(insertAt, 0, ...shuffledPool);
|
||||
return next;
|
||||
});
|
||||
setChronicle(prev => [...prev, { turn: turns, text: '[페이징] 페이즈 2 진입 - 작전 / 극비 프로젝트 카드 풀이 개방되었습니다.' }]);
|
||||
}, [narrativeTags, phase2Pool, phase2Unlocked, currentCardIndex, turns]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '페이징', keyword: null, text: '페이즈 2 진입 - 작전 / 극비 프로젝트 카드 풀이 개방되었습니다.', decision: '', tag: null }]);
|
||||
}, [acquiredTags, phase2Pool, phase2Unlocked, currentCardIndex, turns]);
|
||||
|
||||
useEffect(() => {
|
||||
// 위험 수용 계수(risk)는 PRD상 탄핵/법안 상정의 기준치일 뿐, 직접적인 게임오버 트리거가 아니다.
|
||||
if (!gameOver && (params.resistance >= 5 || params.entropy >= 5 || params.panic >= 5)) {
|
||||
// 위험 수용 계수(risk)는 PRD상 탄핵/법안 상정의 기준치일 뿐, 직접적인 게임오버 트리거가 아니다.
|
||||
const ending = resolveEnding({
|
||||
trigger: 'collapse', params, tags, tokens,
|
||||
lastDelta: lastParamDelta, narrativeChainUnlocked,
|
||||
@@ -203,9 +211,9 @@ export function GameScreen() {
|
||||
setFinalEnding(ending);
|
||||
setGameOver(true);
|
||||
}
|
||||
if (narrativeTags >= PHASE2_TAG_THRESHOLD && narrativeTags < PHASE3_TAG_THRESHOLD) setPhase(2);
|
||||
else if (narrativeTags >= PHASE3_TAG_THRESHOLD) setPhase(3);
|
||||
}, [params, narrativeTags]);
|
||||
if (acquiredTags.length >= PHASE2_TAG_THRESHOLD && acquiredTags.length < PHASE3_TAG_THRESHOLD) setPhase(2);
|
||||
else if (acquiredTags.length >= PHASE3_TAG_THRESHOLD) setPhase(3);
|
||||
}, [params, acquiredTags]);
|
||||
|
||||
// 시한부 격리된 오염 카드 복귀
|
||||
useEffect(() => {
|
||||
@@ -214,7 +222,7 @@ export function GameScreen() {
|
||||
if (ready.length > 0) {
|
||||
setCards(prev => [...prev, ...ready.map(({ returnTurn, ...rest }) => rest)]);
|
||||
setQuarantineZone(prev => prev.filter(c => c.returnTurn > turns));
|
||||
setChronicle(prev => [...prev, { turn: turns, text: `[자산] 격리했던 오염 카드 ${ready.length}건이 덱으로 복귀했습니다.` }]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '자산', keyword: null, text: `격리했던 오염 카드 ${ready.length}건이 덱으로 복귀했습니다.`, decision: '', tag: null }]);
|
||||
}
|
||||
}, [turns]);
|
||||
|
||||
@@ -244,6 +252,8 @@ export function GameScreen() {
|
||||
|
||||
let outcomeText = '';
|
||||
const side = direction === 'left' ? card.left : direction === 'right' ? card.right : null;
|
||||
let isNewTag = false;
|
||||
let gainedTag = null;
|
||||
|
||||
if (side) {
|
||||
const shouldNullify = card.contaminated && mulliganShields > 0;
|
||||
@@ -255,7 +265,7 @@ export function GameScreen() {
|
||||
const nextPersonnel = tokens.personnel + side.stats.per;
|
||||
const nextInfo = tokens.info + side.stats.inf;
|
||||
if (nextBudget < 0 || nextPersonnel < 0 || nextInfo < 0) {
|
||||
setBankruptcyCounter(c => c + 1);
|
||||
setBankruptcyCount(c => c + 1);
|
||||
}
|
||||
|
||||
// 극단적 파라미터 조기 엔딩 판별용: 이번 결재에서 가장 크게 증가한 위험 파라미터 추적
|
||||
@@ -268,10 +278,13 @@ export function GameScreen() {
|
||||
if (maxParam) setLastParamDelta({ param: maxParam, amount: maxVal });
|
||||
}
|
||||
|
||||
// 내러티브 키워드: 모든 카드가 아니라 선택된 일부 카드/선택지만 키워드를 부여한다
|
||||
if (side.narrativeTag) {
|
||||
setKeywords(prev => [...prev, { turn: turns + 1, word: side.narrativeTag }]);
|
||||
setNarrativeTags(n => n + 1);
|
||||
// 내러티브 태그: 재사용 가능한 태그 풀에서 처음 획득하는 태그일 때만 진행도에 반영
|
||||
gainedTag = side.tag;
|
||||
if (gainedTag && !acquiredTags.find(t => t.id === gainedTag.id)) {
|
||||
isNewTag = true;
|
||||
setAcquiredTags(prev => [...prev, gainedTag]);
|
||||
setNewTagNotification(gainedTag);
|
||||
setTimeout(() => setNewTagNotification(null), 3000);
|
||||
}
|
||||
|
||||
// 내러티브 연쇄 엔딩: 오염 카드 4종이 정해진 순서로 등장했는지 추적
|
||||
@@ -280,7 +293,6 @@ export function GameScreen() {
|
||||
setContaminationSequence(nextSequence);
|
||||
if (!narrativeChainUnlocked && isNarrativeChainComplete(nextSequence)) {
|
||||
setNarrativeChainUnlocked(true);
|
||||
setChronicle(prev => [...prev, { turn: turns + 1, text: '[내러티브] 사건들의 연쇄가 완성되었습니다 - 진실에 가까워졌습니다.' }]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +305,10 @@ export function GameScreen() {
|
||||
}
|
||||
|
||||
if (outcomeText) {
|
||||
setChronicle(prev => [...prev, { turn: turns + 1, text: `[${card.type}] ${card.text} -> 결정: ${outcomeText}` }]);
|
||||
setChronicle(prev => [...prev, {
|
||||
turn: turns + 1, cardType: card.type, keyword: card.keyword, text: card.text,
|
||||
decision: outcomeText, tag: isNewTag ? gainedTag : null,
|
||||
}]);
|
||||
}
|
||||
|
||||
// 극비 프로젝트 성공 시 자산 획득 (4종 중 랜덤)
|
||||
@@ -307,11 +322,11 @@ export function GameScreen() {
|
||||
}
|
||||
|
||||
setCurrentCardIndex(idx => idx + 1);
|
||||
setTurns(t => t + 1);
|
||||
|
||||
const newTurns = turns + 1;
|
||||
setTurns(newTurns);
|
||||
|
||||
if (newTurns > 0 && newTurns % 10 === 0) {
|
||||
const newRemaining = approvalsRemaining - 1;
|
||||
setApprovalsRemaining(newRemaining);
|
||||
if (newRemaining <= 0) {
|
||||
setShowCouncil(true);
|
||||
}
|
||||
};
|
||||
@@ -336,21 +351,19 @@ export function GameScreen() {
|
||||
next.splice(insertAt, 0, ...newCards);
|
||||
return next;
|
||||
});
|
||||
setChronicle(prev => [...prev, { turn: turns, text: `[오염] 의회 종료 후 신규 오염 카드 ${newCards.length}건이 덱에 스며들었습니다.` }]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '오염', keyword: null, text: `정례회의 종료 후 신규 오염 카드 ${newCards.length}건이 덱에 스며들었습니다.`, decision: '', tag: null }]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCouncilResolve = (result) => {
|
||||
setShowCouncil(false);
|
||||
|
||||
if (result.bankruptcy) {
|
||||
if (result.bankrupted) {
|
||||
setFinalEnding(getEndingById('bankruptcy'));
|
||||
setGameOver(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setBankruptcyWarned(result.bankruptcyWarning);
|
||||
|
||||
if (result.impeachment) {
|
||||
const ending = resolveEnding({ trigger: 'impeachment', tags, chairmanAxis, narrativeChainUnlocked });
|
||||
setFinalEnding(ending);
|
||||
@@ -358,7 +371,28 @@ export function GameScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof result.bankruptcyWarning === 'boolean') {
|
||||
setBankruptcyWarning(result.bankruptcyWarning);
|
||||
}
|
||||
|
||||
injectContaminationCards();
|
||||
|
||||
// 라운드 진행: 라운드가 증가할수록 다음 정례회의까지의 결재 건수가 줄어든다
|
||||
const nextRound = roundCount + 1;
|
||||
let nextApprovals;
|
||||
if (roundCount === 1) nextApprovals = 8;
|
||||
else if (roundCount === 2) nextApprovals = 6;
|
||||
else if (roundCount === 3) nextApprovals = 4;
|
||||
else if (roundCount === 4) nextApprovals = 2;
|
||||
else nextApprovals = 0;
|
||||
|
||||
setRoundCount(nextRound);
|
||||
setApprovalsRemaining(nextApprovals);
|
||||
|
||||
if (nextApprovals <= 0) {
|
||||
setFinalEnding(getEndingById('term_expired'));
|
||||
setGameOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 자산 효과 4종 발동
|
||||
@@ -366,7 +400,7 @@ export function GameScreen() {
|
||||
if (asset.type === 'mulligan') {
|
||||
setMulliganShields(s => s + 1);
|
||||
setAssets(prev => prev.filter(a => a.id !== asset.id));
|
||||
setChronicle(prev => [...prev, { turn: turns, text: '[자산] 결재권 강제 행사 방어막을 전개했습니다. 다음 오염 카드의 페널티가 무효화됩니다.' }]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '자산', keyword: null, text: '결재권 강제 행사 방어막을 전개했습니다. 다음 오염 카드의 페널티가 무효화됩니다.', decision: '', tag: null }]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -374,7 +408,7 @@ export function GameScreen() {
|
||||
cards.forEach((c, i) => { if (c.contaminated) contaminatedIdx.push(i); });
|
||||
|
||||
if (contaminatedIdx.length === 0) {
|
||||
setChronicle(prev => [...prev, { turn: turns, text: `[자산] ${asset.label}을(를) 발동했지만 대상이 될 오염 카드가 없습니다.` }]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '자산', keyword: null, text: `${asset.label}을(를) 발동했지만 대상이 될 오염 카드가 없습니다.`, decision: '', tag: null }]);
|
||||
setAssets(prev => prev.filter(a => a.id !== asset.id));
|
||||
return;
|
||||
}
|
||||
@@ -383,17 +417,17 @@ export function GameScreen() {
|
||||
const target = contaminatedIdx[Math.floor(Math.random() * contaminatedIdx.length)];
|
||||
setCards(prev => prev.filter((_, i) => i !== target));
|
||||
setPurgeCount(c => c + 1);
|
||||
setChronicle(prev => [...prev, { turn: turns, text: '[자산] 오염 카드를 물리적으로 영구 소각했습니다.' }]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '자산', keyword: null, text: '오염 카드를 물리적으로 영구 소각했습니다.', decision: '', tag: null }]);
|
||||
} else if (asset.type === 'quarantine') {
|
||||
const targetSet = new Set(contaminatedIdx.slice(0, 2));
|
||||
const quarantined = cards.filter((c, i) => targetSet.has(i));
|
||||
setCards(prev => prev.filter((_, i) => !targetSet.has(i)));
|
||||
setQuarantineZone(prev => [...prev, ...quarantined.map(c => ({ ...c, returnTurn: turns + 5 }))]);
|
||||
setChronicle(prev => [...prev, { turn: turns, text: `[자산] 오염 카드 ${quarantined.length}건을 시한부 격리했습니다. (5턴 후 덱으로 복귀)` }]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '자산', keyword: null, text: `오염 카드 ${quarantined.length}건을 시한부 격리했습니다. (5턴 후 덱으로 복귀)`, decision: '', tag: null }]);
|
||||
} else if (asset.type === 'transform') {
|
||||
const target = contaminatedIdx[Math.floor(Math.random() * contaminatedIdx.length)];
|
||||
setCards(prev => prev.map((c, i) => (i === target ? transformCard(c) : c)));
|
||||
setChronicle(prev => [...prev, { turn: turns, text: '[자산] 오염 카드의 성질을 영구적으로 순화했습니다. (독을 약으로)' }]);
|
||||
setChronicle(prev => [...prev, { turn: turns, cardType: '자산', keyword: null, text: '오염 카드의 성질을 영구적으로 순화했습니다. (독을 약으로)', decision: '', tag: null }]);
|
||||
}
|
||||
|
||||
setAssets(prev => prev.filter(a => a.id !== asset.id));
|
||||
@@ -409,8 +443,8 @@ export function GameScreen() {
|
||||
return (
|
||||
<EndingScreen
|
||||
ending={finalEnding}
|
||||
narrativeTags={narrativeTags}
|
||||
keywords={keywords}
|
||||
narrativeTags={acquiredTags.length}
|
||||
keywords={acquiredTags.map(t => ({ word: t.name }))}
|
||||
onRestart={() => window.location.reload()}
|
||||
/>
|
||||
);
|
||||
@@ -422,7 +456,7 @@ export function GameScreen() {
|
||||
tags, tokens, params,
|
||||
assetsCount: assets.length,
|
||||
mythicCount: cards.filter(c => c.type === '신화').length,
|
||||
narrativeTags, assetsAcquiredCount, purgeCount, bribeCount,
|
||||
tagCount: acquiredTags.length, assetsAcquiredCount, purgeCount, bribeCount,
|
||||
};
|
||||
const globalFactions = factionRoster.map(f => {
|
||||
const score = f.computeAffinity(affinityCtx);
|
||||
@@ -435,8 +469,8 @@ export function GameScreen() {
|
||||
params={params} setParams={setParams}
|
||||
tokens={tokens} setTokens={setTokens}
|
||||
tags={tags} setTags={setTags}
|
||||
bankruptcyCounter={bankruptcyCounter}
|
||||
bankruptcyWarned={bankruptcyWarned}
|
||||
bankruptcyCount={bankruptcyCount}
|
||||
bankruptcyWarning={bankruptcyWarning}
|
||||
globalFactions={globalFactions}
|
||||
onBribe={() => setBribeCount(c => c + 1)}
|
||||
onResolve={handleCouncilResolve}
|
||||
@@ -456,7 +490,10 @@ export function GameScreen() {
|
||||
{/* Top Bar */}
|
||||
<div className="top-bar">
|
||||
<div className="narrative-badge" onClick={() => setShowChronicle(true)} style={{cursor: 'pointer'}}>
|
||||
<BookOpen size={16} /> <span>{narrativeTags}{nextTagThreshold !== null ? `/${nextTagThreshold}` : ''}</span>
|
||||
<BookOpen size={16} /> <span>{acquiredTags.length}{nextTagThreshold !== null ? `/${nextTagThreshold}` : ''}</span>
|
||||
</div>
|
||||
<div className="round-info">
|
||||
제 {roundCount}차 정례회의 상정까지: 잔여 {approvalsRemaining}건
|
||||
</div>
|
||||
<div className="parameters">
|
||||
<div className={`param ${params.resistance >= 4 ? 'critical' : ''}`} style={{'--param-color': 'var(--color-resistance-critical)'}}>
|
||||
@@ -518,10 +555,20 @@ export function GameScreen() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{newTagNotification && (
|
||||
<div className="tag-notification">
|
||||
<div className="tag-noti-icon">{newTagNotification.icon}</div>
|
||||
<div className="tag-noti-info">
|
||||
<h4>새로운 내러티브 획득!</h4>
|
||||
<p>[{newTagNotification.name}]</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showChronicle && (
|
||||
<ChroniclePopup
|
||||
chronicle={chronicle}
|
||||
keywords={keywords}
|
||||
acquiredTags={acquiredTags}
|
||||
onClose={() => setShowChronicle(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -50,10 +50,28 @@
|
||||
|
||||
.card-category {
|
||||
font-family: var(--font-typewriter);
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-risk-base);
|
||||
letter-spacing: 2px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-keyword {
|
||||
font-family: var(--font-doc-title);
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
margin: 0 0 8px 0;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.card-question {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 18px;
|
||||
color: #fbbf24;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -64,12 +82,12 @@
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-family: var(--font-doc-title);
|
||||
font-size: 24px;
|
||||
line-height: 1.5;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
color: var(--color-text);
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.swipe-intent {
|
||||
@@ -106,26 +124,31 @@
|
||||
|
||||
.swipe-intent .intent-title {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 8px 16px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid currentColor;
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.card-flavor-container {
|
||||
margin-top: auto;
|
||||
padding-top: 15px;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-flavor {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-typewriter);
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
color: rgba(255,255,255,0.6);
|
||||
color: #a1a1aa;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-preview {
|
||||
@@ -145,10 +168,10 @@
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -156,17 +179,17 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
width: 24px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.stat-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
color: #fff;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
@@ -180,3 +203,27 @@
|
||||
.stat-bad {
|
||||
color: #f87171;
|
||||
}
|
||||
.intent-tag-reward {
|
||||
background: rgba(251, 191, 36, 0.9);
|
||||
color: #000;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
font-family: var(--font-ui);
|
||||
box-shadow: 0 0 15px rgba(251, 191, 36, 0.5);
|
||||
animation: pulseTag 1.5s infinite;
|
||||
}
|
||||
|
||||
.intent-tag-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@keyframes pulseTag {
|
||||
0% { transform: scale(1); box-shadow: 0 0 15px rgba(251, 191, 36, 0.5); }
|
||||
50% { transform: scale(1.05); box-shadow: 0 0 25px rgba(251, 191, 36, 0.8); }
|
||||
100% { transform: scale(1); box-shadow: 0 0 15px rgba(251, 191, 36, 0.5); }
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<span className="card-category">[{card.type}]</span>
|
||||
{card.keyword && <h3 className="card-keyword">{card.keyword}</h3>}
|
||||
{card.question && <h2 className="card-question">{card.question}</h2>}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="card-text">{card.text}</p>
|
||||
@@ -109,10 +111,22 @@ export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
{/* Swipe Intents */}
|
||||
<motion.div className="swipe-intent left-intent" style={{ opacity: leftOpacity }}>
|
||||
<span className="intent-title">{card.left?.text}</span>
|
||||
{card.left?.tag && (
|
||||
<div className="intent-tag-reward">
|
||||
<span className="intent-tag-icon">{card.left.tag.icon}</span>
|
||||
<span className="intent-tag-name">{card.left.tag.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{renderStats(card.left?.stats)}
|
||||
</motion.div>
|
||||
<motion.div className="swipe-intent right-intent" style={{ opacity: rightOpacity }}>
|
||||
<span className="intent-title">{card.right?.text}</span>
|
||||
{card.right?.tag && (
|
||||
<div className="intent-tag-reward">
|
||||
<span className="intent-tag-icon">{card.right.tag.icon}</span>
|
||||
<span className="intent-tag-name">{card.right.tag.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{renderStats(card.right?.stats)}
|
||||
</motion.div>
|
||||
{card.type === '극비' && (
|
||||
|
||||
30
client/src/data/assets.json
Normal file
30
client/src/data/assets.json
Normal file
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"id": "asset_01",
|
||||
"name": "전임 국장의 만년필",
|
||||
"icon": "🖋️",
|
||||
"description": "피로 쓰여진 듯한 잉크가 나옵니다. 예산 관리에 소소한 도움을 줍니다.",
|
||||
"flavor": "\"예산 전용 동의서에 서명하기 아주 좋은 펜이군.\""
|
||||
},
|
||||
{
|
||||
"id": "asset_02",
|
||||
"name": "비밀 스위스 은행 계좌",
|
||||
"icon": "🏦",
|
||||
"description": "마이너스 예산을 한 번 막아줄 수 있습니다.",
|
||||
"flavor": "\"우리 기구가 파산해도 내 노후는 안전해야지.\""
|
||||
},
|
||||
{
|
||||
"id": "asset_03",
|
||||
"name": "기억 소거 장치",
|
||||
"icon": "🔦",
|
||||
"description": "불리한 결재 건을 한 번 취소할 수 있습니다.",
|
||||
"flavor": "\"자, 여기 불빛을 보세요. 당신은 방금 아무것도 보지 못했습니다.\""
|
||||
},
|
||||
{
|
||||
"id": "asset_04",
|
||||
"name": "방탄 커피 머신",
|
||||
"icon": "☕",
|
||||
"description": "요원들의 불만을 약간 억제합니다.",
|
||||
"flavor": "\"아무리 세상이 멸망해도 모닝 커피는 포기 못하지.\""
|
||||
}
|
||||
]
|
||||
@@ -1,49 +1,48 @@
|
||||
id,type,text,flavor,left_text,left_res,left_ent,left_pan,left_rsk,left_bud,left_per,left_inf,left_wea,left_for,left_sur,right_text,right_res,right_ent,right_pan,right_rsk,right_bud,right_per,right_inf,right_wea,right_for,right_sur,grade,left_narrative,right_narrative
|
||||
1,일상,B구역 하급 요원들이 예산 삭감에 반발해 장비 반납 시위를 하고 있습니다.,우리가 세상을 구하는데 월급이 깎인다고요? 촉수에 맞아 죽으나 굶어 죽으나 똑같습니다!,무력 진압,1,0,0,0,0,-10,0,0,0,0,요구 수용,0,0,0,0,-20,0,0,0,0,0
|
||||
2,위기,지하 4층 격리실의 문은 닫혀 있습니다만 요원 12명이 남긴 체액의 총량이 인간 12명의 부피를 초과했습니다.,보고서 첨부: 액체화된 직원은 산재 처리가 안 됩니다.,네이팜 소각 강행,0,0,0,0,0,-30,0,0,0,0,조사팀 파견,0,1,0,1,0,0,0,0,0,0,,증거 인멸 소각,진상 조사 착수
|
||||
3,일상,7구역에 촉수 괴물이 출현했습니다. 작전팀이 법인카드 영수증 제출을 누락해서 출동 승인이 반려된 상태입니다.,규정은 규정입니다. 영수증 없이 세계를 구하는 건 용납할 수 없습니다.,내역 위조 후 출동,0,0,0,1,0,0,-10,0,0,0,규정대로 반려,1,1,0,0,0,0,0,0,0,0
|
||||
4,극비,진화 미래당이 제안합니다. 요원들에게 괴수의 DNA를 소량 이식하면 작전 효율이 300% 상승합니다. 예산은 우리가 대죠.,인간의 한계는 명확합니다. 조금 징그러워지는 것쯤은 감수해야죠.,실험 승인,0,2,0,0,40,40,0,0,0,0,제안 거절,1,0,0,0,0,0,0,0,0,0,,인체 실험 승인,진화 계획 거절
|
||||
5,작전,철의 수호당이 민간인 구역에 전술 폭격을 가해 S급 변칙 개체를 제거하자고 건의했습니다.,희생 없는 승리는 없습니다. 소를 위해 대를... 아니 대를 위해 소를 희생하죠.,궤도 폭격 승인,0,0,0,2,0,0,0,0,2,0,작전 기각,1,0,0,0,0,0,0,0,0,0,,민간인 희생 묵인,온건 노선 고수
|
||||
6,일상,차원 무역당 소속 브로커가 처리된 괴수의 부산물을 암시장에 팔아치우자고 속삭입니다.,이 이빨 하나면 국장님 연봉의 10배입니다. 눈 한 번만 딱 감으시죠.,매각 허가,0,0,0,1,30,0,0,0,0,0,원칙대로 소각,0,0,0,0,-10,0,0,0,0,0
|
||||
7,작전,대중 매체를 장악하여 I.S.C.O의 존재를 서서히 양지화하는 프로젝트입니다. 막대한 정보 토큰이 소모됩니다.,우리가 이 행성의 진짜 지배자임을 알릴 때가 되었습니다.,감시망 구축,0,0,0,0,0,0,-40,0,0,2,프로젝트 보류,1,0,0,0,0,0,0,0,0,0,,조직 양지화 착수,은둔 노선 유지
|
||||
8,극비,고대 언어 해독을 위해 대규모 도서관 건립이 필요합니다. 지식 보존당이 학자들을 동원할 것입니다.,이해할 수 없는 것을 죽이는 건 야만인이나 하는 짓입니다.,예산 배정,0,0,0,0,-40,0,40,0,0,0,예산 전액 삭감,1,0,0,1,0,0,0,0,0,0
|
||||
9,위기,기술 원리주의자들이 마녀사냥을 시작했습니다. 기구 내 마법 사용 요원들을 이단으로 규정하고 색출 중입니다.,더러운 마법과 변이에 의존해선 안 됩니다. 오직 순수한 인간의 힘으로!,적당히 묵인한다,1,0,0,0,0,-30,0,0,0,0,군사력으로 제압,0,0,0,1,0,0,-20,0,0,0
|
||||
10,일상,한 유튜버가 변칙 현상을 촬영해 업로드했습니다. 침묵의 장막당이 즉각적인 물리적 제거와 기억 소거를 요구합니다.,대중은 진실을 감당할 지능이 없습니다. 무덤까지 비밀을 덮으십시오.,제거 절차 승인,0,0,0,-1,0,0,-20,0,0,0,영상만 삭제,0,1,0,0,0,0,0,0,0,0
|
||||
11,위기,특정 구역의 사이비 종교가 고대 신을 소환하기 위해 시민 100명을 인신 공양하려 합니다. 공존당은 이를 방관하라 합니다.,위대한 그분들이 배가 고프다 하십니다. 조금만 양보하면 평화롭습니다.,군사 개입 지시,1,0,1,0,0,-40,0,0,0,0,의식 방관,0,2,0,0,0,0,0,0,0,0
|
||||
12,위기,의회에서 국장의 예산 전용 의혹을 제기하며 긴급 감사를 통보했습니다. 장부를 조작해야 합니다!,세상을 구하는 데 돈 좀 쓴 게 죄입니까? 빨리 덮을 정보팀을 부르십시오.,은폐 공작 실행,0,0,0,1,0,0,-40,0,0,0,횡령 사실 인정,0,0,0,2,0,0,0,0,0,0,,은폐 공작 실행,횡령 자백
|
||||
13,일상,연구 부서에서 초과 근무로 인한 집단 피로를 호소하며 휴가와 특별 보너스를 요구하고 있습니다.,저희도 사람입니다. 괴수 해부하다가 과로사하긴 싫어요.,특별 보너스 지급,-1,0,0,0,-30,0,0,0,0,0,요구 전면 묵살,1,0,0,0,0,0,0,0,0,0
|
||||
14,작전,이계와의 무역로를 임시 개방하여 자원을 조달하자는 안건이 상정되었습니다. 미지의 오염이 우려됩니다.,저 너머의 세계엔 우리가 상상도 못 할 자원이 널려있습니다. 황금알을 낳는 거위죠.,게이트 강제 개방,0,1,0,0,50,0,0,0,0,0,폐쇄 상태 유지,1,0,0,0,0,0,0,0,0,0,,이계 무역로 개방,고립주의 선택
|
||||
15,극비,전임 국장이 남긴 비밀 금고를 발견했습니다. 여는 데 특수 장비와 전문 인력이 필요합니다.,전임 국장님이 숙청당하기 전 남긴 비자금 장부일지도 모릅니다.,강제 개방 지시,0,0,0,0,20,-30,0,1,0,0,영구 봉인,0,0,0,0,0,0,0,0,0,0,,비자금 장부 발견,과거를 봉인
|
||||
16,작전,글로벌 메가코프를 협박해 I.S.C.O의 비자금을 세탁하고 대규모 자금을 확보할 기회입니다.,그들이 우리 덕분에 안전하게 사업하는 겁니다. 보호세 좀 걷는 게 뭐 어떻습니까.,작전 실행,0,0,0,0,0,0,-30,2,0,0,작전 취소,1,0,0,0,0,0,0,0,0,0
|
||||
17,위기,제7구역이 S급 변칙 개체에 의해 완전히 붕괴될 위기입니다. 민간인을 포기하고 융단 폭격을 해야 할까요?,시민 10만 명 구출 비용보다 유가족 합의금이 예산을 15% 더 아낄 수 있습니다.,시민 포기 및 폭격,0,-1,0,2,0,0,0,0,0,0,결사 방어 명령,0,0,0,0,-40,-40,0,0,0,0,,민간인 희생 승인,결사 방어 명령
|
||||
18,일상,새로운 회식비 규정이 너무 짜다며 행정부에서 파업을 예고했습니다. 괴수 사체 처리 지연으로 악취가 진동합니다.,기구의 절반은 서류 작업으로 돌아갑니다. 그들을 화나게 해선 안 됩니다.,예산 전용으로 무마,0,0,0,1,-20,0,0,0,0,0,파업 강경 대응,1,0,0,0,0,0,0,0,0,0
|
||||
19,극비,비밀 연구소에서 인공지능과 죽은 요원의 뇌를 결합한 불침번 요원을 개발했습니다.,잠도 안 자고 월급도 안 받습니다. 윤리적 문제만 빼면 완벽한 자산이죠.,실전 즉각 배치,0,1,0,0,0,0,20,0,1,0,즉각 폐기 처분,1,0,0,0,-10,0,0,0,0,0
|
||||
20,일상,기구 내부에 스파이가 있다는 첩보가 들어왔습니다. 대대적인 사상 검증이 필요할지도 모릅니다.,누군가 정보를 빼돌려 의회에 넘기고 있습니다. 잡히면 믹서기에 넣어야 합니다.,대규모 사찰 시작,2,0,0,0,0,0,30,0,0,0,증거 부족으로 덮음,0,0,0,1,0,0,0,0,0,0,,공포 정치 시작,의혹 은폐
|
||||
21,작전,순수 인간당이 변이의 기미가 보이는 요원들을 격리 수용할 대규모 시설 건립을 제안했습니다.,괴물과 싸우다 보면 괴물이 되는 법. 그들이 선을 넘기 전에 가둬야 합니다.,수용소 건립 승인,0,0,0,0,-40,0,0,0,1,0,제안 단호히 거절,1,0,0,1,0,0,0,0,0,0,,변이체 강제 수용,차별 정책 거부
|
||||
22,극비,고대 공존당의 사제들이 심해의 유물을 인양했습니다. 만지는 자의 정신을 갉아먹지만 막대한 지식을 줍니다.,약간의 두통만 참으면 이계의 비밀을 모두 알 수 있습니다. 요원 하나쯤 미쳐도 괜찮습니다.,유물 집중 연구,0,1,0,0,0,0,50,0,0,0,유물 파괴 명령,1,0,0,1,0,0,0,0,0,0
|
||||
23,위기,얇아진 장막 너머로 거대한 눈동자가 며칠째 지구를 주시하고 있습니다. 대중의 공포가 확산되고 있습니다.,하늘을 보지 마십시오. 아니 차라리 눈을 감고 살게 하십시오.,인터넷 전면 차단,2,0,1,0,0,0,0,0,0,2,진정제 무상 배포,0,0,0,0,-50,0,0,0,0,0
|
||||
24,일상,요원들이 괴수 진압 후 심각한 트라우마에 시달리고 있습니다. 심리 치료 지원이 시급합니다.,밤마다 촉수가 목을 조르는 악몽을 꾼답니다. 차라리 그 부분 기억을 지워버릴까요?,정신과 치료 지원,0,0,0,0,-20,10,0,0,0,0,트라우마 기억 소거,0,1,0,1,0,0,0,0,0,0
|
||||
25,극비,의회 의원 중 한 명이 기구의 비리를 폭로하려 합니다. 침묵의 장막당이 사고사 처리를 제안합니다.,국장님 계단에서 구르거나 심장마비가 오는 건 아주 흔한 자연의 섭리입니다.,사고사 위장 지시,0,0,0,-1,0,0,-30,0,0,0,폭로전 정면 돌파,0,0,0,2,0,0,0,0,0,0,,내부 고발자 암살,비리 자진 시인
|
||||
26,위기,차원 붕괴 폭탄의 타이머가 오작동을 일으켰습니다! 당장 끄지 않으면 7구역이 통째로 증발합니다.,최저가 입찰로 부품을 사서 폭탄을 만든 결과입니다. 누군가는 책임져야 합니다.,특수 해체팀 투입,0,0,0,0,0,-40,0,0,0,0,폭발 방관 및 대피,0,2,0,0,0,0,0,0,0,0,,목숨 건 해체 작전,구역 포기 결정
|
||||
27,작전,군부와 결탁하여 민간 군사 기업(PMC)을 I.S.C.O의 통제 하에 두려는 작전입니다.,돈으로 충성심을 사는 건 가장 안전한 방법입니다. 물론 돈이 많을 때 이야기죠.,결탁 및 흡수 합병,0,0,0,0,-40,0,0,0,2,0,독자적 노선 유지,1,0,0,0,0,0,0,0,0,0
|
||||
28,작전,변칙 개체 관찰을 위해 1구역을 돔으로 덮고 거대한 실험장으로 만들자는 제안이 올라왔습니다.,시민들이 좀 불편해하겠지만 과학적 진보를 위해 그 정도는 감수해야죠.,돔 건설 강행,0,1,0,0,-50,0,0,0,0,0,윤리적 이유로 기각,0,0,0,1,0,0,0,0,0,0
|
||||
29,극비,이계의 군주가 외교 사절을 보내왔습니다. 요원 10명을 노예로 주면 최고급 이계 무기를 주겠다고 합니다.,요원 10명의 생명 가치보다 이계 무기가 가져올 파급력이 월등히 높습니다.,거래 즉시 성사,0,0,0,0,0,-40,0,0,2,0,사절단 전원 사살,0,1,0,1,0,0,0,0,0,0,,요원을 노예로 상납,외교 사절 몰살
|
||||
30,일상,국장님 믹스 커피가 다 떨어졌습니다. 이게 무슨 세상의 종말보다 더 큰 위기인 줄 아십니까? 행정부서의 불만이 폭주합니다.,종말을 결재하기 전엔 커피 한 잔의 여유가 필요한 법입니다. 안 그렇습니까?,최고급 원두 지원,-1,0,0,0,-10,0,0,0,0,0,헛소리 금지령,1,0,0,0,0,0,0,0,0,0
|
||||
31,위기,신화적 존재가 깨어나려 합니다. 하늘이 피처럼 붉게 물들었고 요원들은 미쳐가고 있습니다!,최종 결재의 시간입니다. 세상의 물리 법칙을 비틀 것입니까 아니면 피로 막을 것입니까?,현실 조작기 가동,0,2,0,0,0,0,-50,0,0,0,모든 예비병력 투입,0,0,0,1,0,-60,0,0,0,0,,현실 왜곡 감행,총력전 돌입
|
||||
32,신화,격리된 실험체 하나가 벽을 긁으며 알아들을 수 없는 노래를 흥얼거립니다.,직원들 사이에서 그 노래가 귓가에 맴돈다는 소문이 돕니다.,진정제 투여 및 격리 강화,0,0,0,0,-15,0,0,0,0,0,그냥 무시하고 넘어간다,0,1,0,0,0,0,0,0,0,0,C
|
||||
33,신화,지하 수조에서 건져올린 조각상이 자정마다 미세하게 위치를 바꿉니다.,CCTV 확인 결과 아무도 없었는데 조각상은 분명 돌아서 있었습니다.,석관에 봉인 후 콘크리트 매립,0,0,0,0,-30,-10,0,0,0,0,일단 창고에 방치,0,1,1,0,0,0,0,0,0,0,B
|
||||
34,신화,심해 탐사팀이 인양한 알 하나가 부화 징후를 보입니다. 안에서 무언가 박동합니다.,생물학팀은 저것은 알이 아니라 심장이라고 보고했습니다.,즉각 냉동 처리 및 소각,0,0,0,0,-50,-20,0,0,0,0,부화 과정 관찰 지속,0,2,1,0,0,0,0,0,0,0,A,미지의 알 폐기,금단의 부화 관찰
|
||||
35,신화,장막 너머의 존재가 마침내 자신의 진짜 이름을 발음하기 시작했습니다. 듣는 요원들의 코피가 멈추지 않습니다.,언어병리학자는 이름의 세 음절만 듣고도 실어증에 걸렸습니다.,전 요원 청각 차단 및 격리 봉쇄,0,1,0,0,-70,-40,0,0,0,0,이름을 끝까지 듣고 기록한다,0,3,2,0,0,0,0,0,0,0,S,금단의 이름 봉인,금단의 이름 기록
|
||||
36,조직저항,현장 요원 셋이 초과근무수당 미지급에 항의하며 보고서 제출을 하루 미뤘습니다.,정시 퇴근이 인권이라는 팻말을 봤습니다.,밀린 수당 즉시 지급,0,0,0,0,-15,0,0,0,0,0,규정 운운하며 묵살,1,0,0,0,0,0,0,0,0,0,C
|
||||
37,조직저항,중간관리자 다섯이 연명으로 국장의 승인 절차에 대한 항명 성명을 냈습니다.,서명자 명단에 낯익은 이름이 보입니다. 승진 대상자였는데요.,일대일 면담으로 무마,0,0,0,0,0,-25,0,0,0,0,주동자 색출 및 좌천,2,0,0,0,0,0,0,0,0,0,B
|
||||
38,조직저항,격리동 경비 인력 절반이 동시에 병가를 제출했습니다. 명백한 태업입니다.,다들 같은 날 같은 감기에 걸렸다고 합니다. 참으로 공교롭죠.,대체 인력 긴급 투입,0,0,0,0,-30,-15,0,0,0,0,복귀할 때까지 방치,2,0,1,0,0,0,0,0,0,0,A,태업 진압,조직 와해 방관
|
||||
39,조직저항,전 부서장급 열두 명이 동시 사표를 제출하며 내부 기밀을 언론에 흘리겠다 협박합니다.,이것은 항명이 아니라 쿠데타입니다.,전원 요구 수용 및 대규모 숙청,1,0,0,0,-60,-50,0,0,0,0,협박을 무시하고 강경 대응,3,1,0,0,0,0,0,0,0,0,S,대숙청 단행,쿠데타 조짐
|
||||
40,사회공황,동네 커뮤니티에 하늘에서 이상한 빛을 봤다는 글이 올라와 조회수가 치솟고 있습니다.,댓글 반응은 반반입니다. 반은 조작설 반은 목격담 추가.,여론 조작팀 투입해 진화,0,0,0,0,0,0,-15,0,0,0,며칠 지나면 묻힐 겁니다,0,0,1,0,0,0,0,0,0,0,C
|
||||
41,사회공황,한 지역 방송국이 괴수 목격담을 특집 뉴스로 다뤘습니다. 시청률이 세 배 뛰었습니다.,미스터리 특종이라는 자막이 하루 종일 반복 재생됩니다.,방송국 인수 후 정정 보도,0,0,0,0,-30,0,0,0,0,0,허위 정보로 규정하고 방치,0,0,2,0,0,0,0,0,0,0,B
|
||||
42,사회공황,대형 마트마다 생필품 사재기 행렬이 이어지고 있습니다. 폭동 조짐이 보입니다.,화장지 한 팩을 두고 몸싸움이 벌어졌다는 신고가 열 건째입니다.,비상 물자 방출 및 진정,0,0,0,0,-40,0,-15,0,0,0,질서 유지는 각자 알아서,1,0,2,0,0,0,0,0,0,0,A,비상 물자 방출,치안 붕괴 방관
|
||||
43,사회공황,종말론에 심취한 신도 수천 명이 본부 앞으로 행진하며 심판을 요구합니다.,그들의 팻말에는 이상하게도 정확한 좌표가 적혀 있었습니다.,전면 계엄 수준의 통제 발동,0,1,1,0,-50,-30,0,0,0,0,군중 앞에서 진실을 공개한다,0,1,3,0,0,0,0,0,0,0,S,계엄 선포,진실 공개 선언
|
||||
44,이사회압박,감사실에서 지난달 접대비 영수증 몇 장을 문제 삼기 시작했습니다.,법인카드로 산 초밥이 감사 보고서 제목이 됐습니다.,영수증 재발급 후 해명,0,0,0,0,-15,0,0,0,0,0,바쁘다는 핑계로 무시,0,0,0,1,0,0,0,0,0,0,C
|
||||
45,이사회압박,이사회가 예산 집행 내역 전수 조사를 예고했습니다.,회계팀 막내가 벌써부터 사직서 초안을 쓰고 있습니다.,회계 자료 사전 정리,0,0,0,0,0,-25,0,0,0,0,조사에 순순히 응한다,0,0,0,2,0,0,0,0,0,0,B
|
||||
46,이사회압박,긴급 임시 이사회가 소집되어 국장의 결재권 일부 정지를 논의합니다.,의제 제목이 국장의 독단 경영에 대하여입니다.,핵심 임원 개별 설득,0,0,0,0,-40,0,-15,0,0,0,정면으로 맞선다,1,0,0,2,0,0,0,0,0,0,A,막후 로비,이사회와 정면 충돌
|
||||
47,이사회압박,청문회 소환장이 도착했습니다. 결재권 자체를 박탈하겠다는 초강경 안건입니다.,출석하지 않으면 그 자체로 유죄로 간주하겠다는 문구가 적혀 있습니다.,총력을 다해 청문회 방어,0,0,0,1,-60,-30,-20,0,0,0,청문회를 보이콧한다,1,1,0,3,0,0,0,0,0,0,S,청문회 사수,청문회 보이콧
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
id,type,keyword,question,text,flavor,left_text,left_tag,left_res,left_ent,left_pan,left_rsk,left_bud,left_per,left_inf,left_wea,left_for,left_sur,right_text,right_tag,right_res,right_ent,right_pan,right_rsk,right_bud,right_per,right_inf,right_wea,right_for,right_sur,grade
|
||||
1,일상,요원들의 시위 발생,시위를 어떻게 처리하시겠습니까?,B구역 하급 요원들이 예산 삭감에 반발해 장비 반납 시위를 하고 있습니다.,우리가 세상을 구하는데 월급이 깎인다고요? 촉수에 맞아 죽으나 굶어 죽으나 똑같습니다!,무력 진압,tag_ruthless,1,0,0,0,0,-10,0,0,0,0,요구 수용,,0,0,0,0,-20,0,0,0,0,0,
|
||||
2,위기,격리실 오염 발생,오염을 어떻게 정화하시겠습니까?,지하 4층 격리실의 문은 닫혀 있습니다만 요원 12명이 남긴 체액의 총량이 인간 12명의 부피를 초과했습니다.,보고서 첨부: 액체화된 직원은 산재 처리가 안 됩니다.,네이팜 소각 강행,tag_coverup,0,0,0,0,0,-30,0,0,0,0,조사팀 파견,,0,1,0,1,0,0,0,0,0,0,
|
||||
3,일상,촉수 괴물 출현,출동 승인하시겠습니까?,7구역에 촉수 괴물이 출현했습니다. 작전팀이 법인카드 영수증 제출을 누락해서 출동 승인이 반려된 상태입니다.,규정은 규정입니다. 영수증 없이 세계를 구하는 건 용납할 수 없습니다.,내역 위조 후 출동,,0,0,0,1,0,0,-10,0,0,0,규정대로 반려,,1,1,0,0,0,0,0,0,0,0,
|
||||
4,극비,변이 DNA 이식 제안,실험을 승인하시겠습니까?,진화 미래당이 제안합니다. 요원들에게 괴수의 DNA를 소량 이식하면 작전 효율이 300% 상승합니다. 예산은 우리가 대죠.,인간의 한계는 명확합니다. 조금 징그러워지는 것쯤은 감수해야죠.,실험 승인,tag_madness,0,2,0,0,40,40,0,0,0,0,제안 거절,,1,0,0,0,0,0,0,0,0,0,
|
||||
5,작전,전술 폭격 건의,폭격을 승인하시겠습니까?,철의 수호당이 민간인 구역에 전술 폭격을 가해 S급 변칙 개체를 제거하자고 건의했습니다.,희생 없는 승리는 없습니다. 소를 위해 대를... 아니 대를 위해 소를 희생하죠.,궤도 폭격 승인,tag_sacrifice,0,0,0,2,0,0,0,0,2,0,작전 기각,,1,0,0,0,0,0,0,0,0,0,
|
||||
6,일상,부산물 암시장 매각,매각을 허가하시겠습니까?,차원 무역당 소속 브로커가 처리된 괴수의 부산물을 암시장에 팔아치우자고 속삭입니다.,이 이빨 하나면 국장님 연봉의 10배입니다. 눈 한 번만 딱 감으시죠.,매각 허가,tag_corruption,0,0,0,1,30,0,0,0,0,0,원칙대로 소각,,0,0,0,0,-10,0,0,0,0,0,
|
||||
7,작전,존재 양지화 프로젝트,프로젝트를 승인하시겠습니까?,대중 매체를 장악하여 I.S.C.O의 존재를 서서히 양지화하는 프로젝트입니다. 막대한 정보 토큰이 소모됩니다.,우리가 이 행성의 진짜 지배자임을 알릴 때가 되었습니다.,감시망 구축,tag_coverup,0,0,0,0,0,0,-40,0,0,2,프로젝트 보류,,1,0,0,0,0,0,0,0,0,0,
|
||||
8,극비,대규모 도서관 건립,예산을 배정하시겠습니까?,고대 언어 해독을 위해 대규모 도서관 건립이 필요합니다. 지식 보존당이 학자들을 동원할 것입니다.,이해할 수 없는 것을 죽이는 건 야만인이나 하는 짓입니다.,예산 배정,,0,0,0,0,-40,0,40,0,0,0,예산 전액 삭감,,1,0,0,1,0,0,0,0,0,0,
|
||||
9,위기,이단 색출 마녀사냥,어떻게 개입하시겠습니까?,기술 원리주의자들이 마녀사냥을 시작했습니다. 기구 내 마법 사용 요원들을 이단으로 규정하고 색출 중입니다.,더러운 마법과 변이에 의존해선 안 됩니다. 오직 순수한 인간의 힘으로!,적당히 묵인한다,,1,0,0,0,0,-30,0,0,0,0,군사력으로 제압,,0,0,0,1,0,0,-20,0,0,0,
|
||||
10,일상,변칙 현상 영상 유출,어떻게 처리하시겠습니까?,한 유튜버가 변칙 현상을 촬영해 업로드했습니다. 침묵의 장막당이 즉각적인 물리적 제거와 기억 소거를 요구합니다.,대중은 진실을 감당할 지능이 없습니다. 무덤까지 비밀을 덮으십시오.,제거 절차 승인,tag_coverup,0,0,0,-1,0,0,-20,0,0,0,영상만 삭제,,0,1,0,0,0,0,0,0,0,0,
|
||||
11,위기,사이비 종교 인신 공양,어떻게 대응하시겠습니까?,특정 구역의 사이비 종교가 고대 신을 소환하기 위해 시민 100명을 인신 공양하려 합니다. 공존당은 이를 방관하라 합니다.,위대한 그분들이 배가 고프다 하십니다. 조금만 양보하면 평화롭습니다.,군사 개입 지시,,1,0,1,0,0,-40,0,0,0,0,의식 방관,tag_cult,0,2,0,0,0,0,0,0,0,0,
|
||||
12,위기,의회의 긴급 감사,어떻게 대처하시겠습니까?,의회에서 국장의 예산 전용 의혹을 제기하며 긴급 감사를 통보했습니다. 장부를 조작해야 합니다!,세상을 구하는 데 돈 좀 쓴 게 죄입니까? 빨리 덮을 정보팀을 부르십시오.,은폐 공작 실행,tag_corruption,0,0,0,1,0,0,-40,0,0,0,횡령 사실 인정,,0,0,0,2,0,0,0,0,0,0,
|
||||
13,일상,연구 부서 파업 조짐,요구를 수용하시겠습니까?,연구 부서에서 초과 근무로 인한 집단 피로를 호소하며 휴가와 특별 보너스를 요구하고 있습니다.,저희도 사람입니다. 괴수 해부하다가 과로사하긴 싫어요.,특별 보너스 지급,,-1,0,0,0,-30,0,0,0,0,0,요구 전면 묵살,,1,0,0,0,0,0,0,0,0,0,
|
||||
14,작전,이계 무역로 개방,무역로를 개방하시겠습니까?,이계와의 무역로를 임시 개방하여 자원을 조달하자는 안건이 상정되었습니다. 미지의 오염이 우려됩니다.,저 너머의 세계엔 우리가 상상도 못 할 자원이 널려있습니다. 황금알을 낳는 거위죠.,게이트 강제 개방,tag_alien_contact,0,1,0,0,50,0,0,0,0,0,폐쇄 상태 유지,,1,0,0,0,0,0,0,0,0,0,
|
||||
15,극비,전임 국장 비밀 금고,금고를 개방하시겠습니까?,전임 국장이 남긴 비밀 금고를 발견했습니다. 여는 데 특수 장비와 전문 인력이 필요합니다.,전임 국장님이 숙청당하기 전 남긴 비자금 장부일지도 모릅니다.,강제 개방 지시,tag_corruption,0,0,0,0,20,-30,0,1,0,0,영구 봉인,,0,0,0,0,0,0,0,0,0,0,
|
||||
16,작전,메가코프 협박 작전,작전을 실행하시겠습니까?,글로벌 메가코프를 협박해 I.S.C.O의 비자금을 세탁하고 대규모 자금을 확보할 기회입니다.,그들이 우리 덕분에 안전하게 사업하는 겁니다. 보호세 좀 걷는 게 뭐 어떻습니까.,작전 실행,,0,0,0,0,0,0,-30,2,0,0,작전 취소,,1,0,0,0,0,0,0,0,0,0,
|
||||
17,위기,전술 폭격 건의,폭격을 승인하시겠습니까?,제7구역이 S급 변칙 개체에 의해 완전히 붕괴될 위기입니다. 민간인을 포기하고 융단 폭격을 해야 할까요?,시민 10만 명 구출 비용보다 유가족 합의금이 예산을 15% 더 아낄 수 있습니다.,시민 포기 및 폭격,tag_ruthless,0,-1,0,2,0,0,0,0,0,0,결사 방어 명령,,0,0,0,0,-40,-40,0,0,0,0,
|
||||
18,일상,행정부 파업 예고,어떻게 대응하시겠습니까?,새로운 회식비 규정이 너무 짜다며 행정부에서 파업을 예고했습니다. 괴수 사체 처리 지연으로 악취가 진동합니다.,기구의 절반은 서류 작업으로 돌아갑니다. 그들을 화나게 해선 안 됩니다.,예산 전용으로 무마,,0,0,0,1,-20,0,0,0,0,0,파업 강경 대응,,1,0,0,0,0,0,0,0,0,0,
|
||||
19,극비,불침번 요원 개발,배치하시겠습니까?,비밀 연구소에서 인공지능과 죽은 요원의 뇌를 결합한 불침번 요원을 개발했습니다.,잠도 안 자고 월급도 안 받습니다. 윤리적 문제만 빼면 완벽한 자산이죠.,실전 즉각 배치,,0,1,0,0,0,0,20,0,1,0,즉각 폐기 처분,,1,0,0,0,-10,0,0,0,0,0,
|
||||
20,일상,내부 스파이 첩보,어떻게 조사하시겠습니까?,기구 내부에 스파이가 있다는 첩보가 들어왔습니다. 대대적인 사상 검증이 필요할지도 모릅니다.,누군가 정보를 빼돌려 의회에 넘기고 있습니다. 잡히면 믹서기에 넣어야 합니다.,대규모 사찰 시작,tag_political_pressure,2,0,0,0,0,0,30,0,0,0,증거 부족으로 덮음,,0,0,0,1,0,0,0,0,0,0,
|
||||
21,작전,비정상적 위기 발생,국장님 결단이 필요합니다,순수 인간당이 변이의 기미가 보이는 요원들을 격리 수용할 대규모 시설 건립을 제안했습니다.,괴물과 싸우다 보면 괴물이 되는 법. 그들이 선을 넘기 전에 가둬야 합니다.,수용소 건립 승인,tag_ruthless,0,0,0,0,-40,0,0,0,1,0,제안 단호히 거절,,1,0,0,1,0,0,0,0,0,0,
|
||||
22,극비,이계의 유물 인양,유물을 어떻게 처리하시겠습니까?,고대 공존당의 사제들이 심해의 유물을 인양했습니다. 만지는 자의 정신을 갉아먹지만 막대한 지식을 줍니다.,약간의 두통만 참으면 이계의 비밀을 모두 알 수 있습니다. 요원 하나쯤 미쳐도 괜찮습니다.,유물 집중 연구,tag_alien_contact,0,1,0,0,0,0,50,0,0,0,유물 파괴 명령,,1,0,0,1,0,0,0,0,0,0,
|
||||
23,위기,이계의 주시자 출현,대중을 어떻게 통제하시겠습니까?,얇아진 장막 너머로 거대한 눈동자가 며칠째 지구를 주시하고 있습니다. 대중의 공포가 확산되고 있습니다.,하늘을 보지 마십시오. 아니 차라리 눈을 감고 살게 하십시오.,인터넷 전면 차단,,2,0,1,0,0,0,0,0,0,2,진정제 무상 배포,,0,0,0,0,-50,0,0,0,0,0,
|
||||
24,일상,요원 집단 트라우마,어떻게 지원하시겠습니까?,요원들이 괴수 진압 후 심각한 트라우마에 시달리고 있습니다. 심리 치료 지원이 시급합니다.,밤마다 촉수가 목을 조르는 악몽을 꾼답니다. 차라리 그 부분 기억을 지워버릴까요?,정신과 치료 지원,,0,0,0,0,-20,10,0,0,0,0,트라우마 기억 소거,,0,1,0,1,0,0,0,0,0,0,
|
||||
25,극비,내부 고발자 발생,어떻게 막으시겠습니까?,의회 의원 중 한 명이 기구의 비리를 폭로하려 합니다. 침묵의 장막당이 사고사 처리를 제안합니다.,국장님 계단에서 구르거나 심장마비가 오는 건 아주 흔한 자연의 섭리입니다.,사고사 위장 지시,tag_coverup,0,0,0,-1,0,0,-30,0,0,0,폭로전 정면 돌파,,0,0,0,2,0,0,0,0,0,0,
|
||||
26,위기,차원 붕괴 폭탄 오작동,어떻게 대응하시겠습니까?,차원 붕괴 폭탄의 타이머가 오작동을 일으켰습니다! 당장 끄지 않으면 7구역이 통째로 증발합니다.,최저가 입찰로 부품을 사서 폭탄을 만든 결과입니다. 누군가는 책임져야 합니다.,특수 해체팀 투입,tag_sacrifice,0,0,0,0,0,-40,0,0,0,0,폭발 방관 및 대피,,0,2,0,0,0,0,0,0,0,0,
|
||||
27,작전,PMC 흡수 합병,결탁하시겠습니까?,군부와 결탁하여 민간 군사 기업(PMC)을 I.S.C.O의 통제 하에 두려는 작전입니다.,돈으로 충성심을 사는 건 가장 안전한 방법입니다. 물론 돈이 많을 때 이야기죠.,결탁 및 흡수 합병,,0,0,0,0,-40,0,0,0,2,0,독자적 노선 유지,,1,0,0,0,0,0,0,0,0,0,
|
||||
28,작전,대규모 거주 구역 실험장화,제안을 승인하시겠습니까?,변칙 개체 관찰을 위해 1구역을 돔으로 덮고 거대한 실험장으로 만들자는 제안이 올라왔습니다.,시민들이 좀 불편해하겠지만 과학적 진보를 위해 그 정도는 감수해야죠.,돔 건설 강행,,0,1,0,0,-50,0,0,0,0,0,윤리적 이유로 기각,,0,0,0,1,0,0,0,0,0,0,
|
||||
29,극비,이계 군주의 외교,제안을 수락하시겠습니까?,이계의 군주가 외교 사절을 보내왔습니다. 요원 10명을 노예로 주면 최고급 이계 무기를 주겠다고 합니다.,요원 10명의 생명 가치보다 이계 무기가 가져올 파급력이 월등히 높습니다.,거래 즉시 성사,tag_alien_contact,0,0,0,0,0,-40,0,0,2,0,사절단 전원 사살,,0,1,0,1,0,0,0,0,0,0,
|
||||
30,일상,믹스 커피 재고 고갈,어떻게 해결하시겠습니까?,국장님 믹스 커피가 다 떨어졌습니다. 이게 무슨 세상의 종말보다 더 큰 위기인 줄 아십니까? 행정부서의 불만이 폭주합니다.,종말을 결재하기 전엔 커피 한 잔의 여유가 필요한 법입니다. 안 그렇습니까?,최고급 원두 지원,,-1,0,0,0,-10,0,0,0,0,0,헛소리 금지령,,1,0,0,0,0,0,0,0,0,0,
|
||||
31,위기,신화적 존재 강림,어떻게 대적하시겠습니까?,신화적 존재가 깨어나려 합니다. 하늘이 피처럼 붉게 물들었고 요원들은 미쳐가고 있습니다!,최종 결재의 시간입니다. 세상의 물리 법칙을 비틀 것입니까 아니면 피로 막을 것입니까?,현실 조작기 가동,tag_madness,0,2,0,0,0,0,-50,0,0,0,모든 예비병력 투입,tag_sacrifice,0,0,0,1,0,-60,0,0,0,0,
|
||||
32,신화,실험체의 노래,어떻게 조치하시겠습니까?,격리된 실험체 하나가 벽을 긁으며 알아들을 수 없는 노래를 흥얼거립니다.,직원들 사이에서 그 노래가 귓가에 맴돈다는 소문이 돕니다.,진정제 투여 및 격리 강화,,0,0,0,0,-15,0,0,0,0,0,그냥 무시하고 넘어간다,,0,1,0,0,0,0,0,0,0,0,C
|
||||
33,신화,움직이는 조각상,어떻게 처리하시겠습니까?,지하 수조에서 건져올린 조각상이 자정마다 미세하게 위치를 바꿉니다.,CCTV 확인 결과 아무도 없었는데 조각상은 분명 돌아서 있었습니다.,석관에 봉인 후 콘크리트 매립,,0,0,0,0,-30,-10,0,0,0,0,일단 창고에 방치,,0,1,1,0,0,0,0,0,0,0,B
|
||||
34,신화,심해의 알,부화를 막으시겠습니까?,심해 탐사팀이 인양한 알 하나가 부화 징후를 보입니다. 안에서 무언가 박동합니다.,생물학팀은 저것은 알이 아니라 심장이라고 보고했습니다.,즉각 냉동 처리 및 소각,,0,0,0,0,-50,-20,0,0,0,0,부화 과정 관찰 지속,tag_alien_contact,0,2,1,0,0,0,0,0,0,0,A
|
||||
35,신화,장막 너머의 이름,이름을 들으시겠습니까?,장막 너머의 존재가 마침내 자신의 진짜 이름을 발음하기 시작했습니다. 듣는 요원들의 코피가 멈추지 않습니다.,언어병리학자는 이름의 세 음절만 듣고도 실어증에 걸렸습니다.,전 요원 청각 차단 및 격리 봉쇄,tag_madness,0,1,0,0,-70,-40,0,0,0,0,이름을 끝까지 듣고 기록한다,tag_alien_contact,0,3,2,0,0,0,0,0,0,0,S
|
||||
36,조직저항,초과근무 항의,어떻게 무마하시겠습니까?,현장 요원 셋이 초과근무수당 미지급에 항의하며 보고서 제출을 하루 미뤘습니다.,정시 퇴근이 인권이라는 팻말을 봤습니다.,밀린 수당 즉시 지급,,0,0,0,0,-15,0,0,0,0,0,규정 운운하며 묵살,,1,0,0,0,0,0,0,0,0,0,C
|
||||
37,조직저항,항명 성명,어떻게 대응하시겠습니까?,중간관리자 다섯이 연명으로 국장의 승인 절차에 대한 항명 성명을 냈습니다.,서명자 명단에 낯익은 이름이 보입니다. 승진 대상자였는데요.,일대일 면담으로 무마,,0,0,0,0,0,-25,0,0,0,0,주동자 색출 및 좌천,,2,0,0,0,0,0,0,0,0,0,B
|
||||
38,조직저항,경비 태업,어떻게 수습하시겠습니까?,격리동 경비 인력 절반이 동시에 병가를 제출했습니다. 명백한 태업입니다.,다들 같은 날 같은 감기에 걸렸다고 합니다. 참으로 공교롭죠.,대체 인력 긴급 투입,,0,0,0,0,-30,-15,0,0,0,0,복귀할 때까지 방치,tag_coup,2,0,1,0,0,0,0,0,0,0,A
|
||||
39,조직저항,대규모 사표,어떻게 진압하시겠습니까?,전 부서장급 열두 명이 동시 사표를 제출하며 내부 기밀을 언론에 흘리겠다 협박합니다.,이것은 항명이 아니라 쿠데타입니다.,전원 요구 수용 및 대규모 숙청,tag_ruthless,1,0,0,0,-60,-50,0,0,0,0,협박을 무시하고 강경 대응,tag_coup,3,1,0,0,0,0,0,0,0,0,S
|
||||
40,사회공황,괴이한 목격담,어떻게 진화하시겠습니까?,동네 커뮤니티에 하늘에서 이상한 빛을 봤다는 글이 올라와 조회수가 치솟고 있습니다.,댓글 반응은 반반입니다. 반은 조작설 반은 목격담 추가.,여론 조작팀 투입해 진화,,0,0,0,0,0,0,-15,0,0,0,며칠 지나면 묻힐 겁니다,,0,0,1,0,0,0,0,0,0,0,C
|
||||
41,사회공황,특집 뉴스 보도,어떻게 대응하시겠습니까?,한 지역 방송국이 괴수 목격담을 특집 뉴스로 다뤘습니다. 시청률이 세 배 뛰었습니다.,미스터리 특종이라는 자막이 하루 종일 반복 재생됩니다.,방송국 인수 후 정정 보도,,0,0,0,0,-30,0,0,0,0,0,허위 정보로 규정하고 방치,,0,0,2,0,0,0,0,0,0,0,B
|
||||
42,사회공황,생필품 사재기,어떻게 진정시키시겠습니까?,대형 마트마다 생필품 사재기 행렬이 이어지고 있습니다. 폭동 조짐이 보입니다.,화장지 한 팩을 두고 몸싸움이 벌어졌다는 신고가 열 건째입니다.,비상 물자 방출 및 진정,,0,0,0,0,-40,0,-15,0,0,0,질서 유지는 각자 알아서,tag_panic_mob,1,0,2,0,0,0,0,0,0,0,A
|
||||
43,사회공황,종말론 행진,어떻게 통제하시겠습니까?,종말론에 심취한 신도 수천 명이 본부 앞으로 행진하며 심판을 요구합니다.,그들의 팻말에는 이상하게도 정확한 좌표가 적혀 있었습니다.,전면 계엄 수준의 통제 발동,tag_ruthless,0,1,1,0,-50,-30,0,0,0,0,군중 앞에서 진실을 공개한다,tag_panic_mob,0,1,3,0,0,0,0,0,0,0,S
|
||||
44,이사회압박,접대비 감사,어떻게 해명하시겠습니까?,감사실에서 지난달 접대비 영수증 몇 장을 문제 삼기 시작했습니다.,법인카드로 산 초밥이 감사 보고서 제목이 됐습니다.,영수증 재발급 후 해명,,0,0,0,0,-15,0,0,0,0,0,바쁘다는 핑계로 무시,,0,0,0,1,0,0,0,0,0,0,C
|
||||
45,이사회압박,예산 전수조사,어떻게 대비하시겠습니까?,이사회가 예산 집행 내역 전수 조사를 예고했습니다.,회계팀 막내가 벌써부터 사직서 초안을 쓰고 있습니다.,회계 자료 사전 정리,,0,0,0,0,0,-25,0,0,0,0,조사에 순순히 응한다,,0,0,0,2,0,0,0,0,0,0,B
|
||||
46,이사회압박,결재권 정지 논의,어떻게 방어하시겠습니까?,긴급 임시 이사회가 소집되어 국장의 결재권 일부 정지를 논의합니다.,의제 제목이 국장의 독단 경영에 대하여입니다.,핵심 임원 개별 설득,tag_corruption,0,0,0,0,-40,0,-15,0,0,0,정면으로 맞선다,,1,0,0,2,0,0,0,0,0,0,A
|
||||
47,이사회압박,청문회 소환,어떻게 대응하시겠습니까?,청문회 소환장이 도착했습니다. 결재권 자체를 박탈하겠다는 초강경 안건입니다.,출석하지 않으면 그 자체로 유죄로 간주하겠다는 문구가 적혀 있습니다.,총력을 다해 청문회 방어,,0,0,0,1,-60,-30,-20,0,0,0,청문회를 보이콧한다,tag_political_pressure,1,1,0,3,0,0,0,0,0,0,S
|
||||
|
||||
|
@@ -69,6 +69,10 @@ export const ENDING_CATALOG = [
|
||||
desc: '망설임 없는 결정 한 번이 모든 단계를 건너뛰었습니다. 경고도, 예고도 없이 — 그저 끝났습니다.',
|
||||
reason: '단일 결정으로 파라미터가 급격히 5단계에 도달' },
|
||||
|
||||
{ id: 'term_expired', title: '임기 만료 (조용한 퇴장)',
|
||||
desc: '붕괴도, 탄핵도 없었습니다. 그저 라운드가 거듭될수록 결재할 시간이 줄어들었고, 결국 정례회의 상정 건수가 바닥났습니다. 세상은 여전히 위태롭지만, 당신의 임기만은 조용히 끝났습니다.',
|
||||
reason: '라운드가 거듭되며 정례회의까지의 결재 건수가 0이 됨' },
|
||||
|
||||
{ id: 'unidentified', title: '미확인 종말',
|
||||
desc: '기록되지 않은 파멸입니다.',
|
||||
reason: '알 수 없는 원인' },
|
||||
|
||||
62
client/src/data/narrative_tags.json
Normal file
62
client/src/data/narrative_tags.json
Normal file
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"id": "tag_ruthless",
|
||||
"name": "피도 눈물도 없는 진압",
|
||||
"icon": "🩸",
|
||||
"description": "반발을 무력으로 짓밟았습니다. 공포는 훌륭한 통제 수단입니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_coverup",
|
||||
"name": "진실의 장막",
|
||||
"icon": "🤫",
|
||||
"description": "대중이 알지 못하도록 변칙 사건을 완벽하게 은폐했습니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_alien_contact",
|
||||
"name": "금지된 지식",
|
||||
"icon": "👁️",
|
||||
"description": "이계의 존재와 거래하거나 그들의 유물을 받아들였습니다. 장막 너머의 눈이 당신을 주시합니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_corruption",
|
||||
"name": "검은 뒷돈",
|
||||
"icon": "💼",
|
||||
"description": "공적인 자산을 사적으로 유용하거나 부당한 거래를 승인했습니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_madness",
|
||||
"name": "이성의 붕괴",
|
||||
"icon": "🌀",
|
||||
"description": "요원들을 비인도적인 실험과 광기에 노출시켰습니다. 인간성은 점점 희미해집니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_cult",
|
||||
"name": "사이비의 그림자",
|
||||
"icon": "🌑",
|
||||
"description": "고대 신을 숭배하는 종교와 엮이기 시작했습니다. 그들의 교리가 스며듭니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_coup",
|
||||
"name": "쿠데타의 그림자",
|
||||
"icon": "🗡️",
|
||||
"description": "권력자들이 등을 돌리기 시작했습니다. 칼끝이 서서히 당신을 향하고 있습니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_panic_mob",
|
||||
"name": "군중의 광기",
|
||||
"icon": "😱",
|
||||
"description": "이성을 잃은 대중이 거리를 뒤덮었습니다. 더 이상 통제할 수 없습니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_political_pressure",
|
||||
"name": "정치적 압박",
|
||||
"icon": "⚖️",
|
||||
"description": "이사회가 당신의 자리를 노리고 있습니다. 숫자 하나하나가 무기가 됩니다."
|
||||
},
|
||||
{
|
||||
"id": "tag_sacrifice",
|
||||
"name": "희생의 대가",
|
||||
"icon": "⚰️",
|
||||
"description": "누군가의 목숨과 맞바꾼 결정이었습니다. 그 무게는 사라지지 않습니다."
|
||||
}
|
||||
]
|
||||
34
client/src/data/pollution_cards.json
Normal file
34
client/src/data/pollution_cards.json
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"id": "poll_1",
|
||||
"type": "오염",
|
||||
"keyword": "장막 틈새의 오염",
|
||||
"question": "알 수 없는 속삭임이 들립니다...",
|
||||
"text": "사무실 벽에 기괴한 기하학적 무늬가 자라나고 있습니다. 쳐다보기만 해도 구역질이 납니다.",
|
||||
"flavor": "\"저건 분명히 내 업무 일지보다 복잡한 구조야.\"",
|
||||
"left": {
|
||||
"text": "방치한다",
|
||||
"stats": { "ent": 1, "pan": 1, "rsk": 1 }
|
||||
},
|
||||
"right": {
|
||||
"text": "벽을 태운다",
|
||||
"stats": { "bud": -20, "res": 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "poll_2",
|
||||
"type": "오염",
|
||||
"keyword": "부패하는 자원",
|
||||
"question": "금고에서 악취가 납니다...",
|
||||
"text": "예산으로 배정된 금괴들이 살덩어리로 변했습니다. 이대로는 결재에 사용할 수 없습니다.",
|
||||
"flavor": "\"음, 적어도 인력 갈아넣는 것보단 싸게 먹히겠군.\"",
|
||||
"left": {
|
||||
"text": "폐기 처분",
|
||||
"stats": { "bud": -50 }
|
||||
},
|
||||
"right": {
|
||||
"text": "억지로 사용",
|
||||
"stats": { "ent": 2, "rsk": 2 }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -6,22 +6,49 @@ const parsed = Papa.parse(csv, { header: true });
|
||||
|
||||
parsed.data.forEach(row => {
|
||||
if (!row.id) return;
|
||||
row.left_pan = 0;
|
||||
row.right_pan = 0;
|
||||
// Let's add panic to some rows for testing
|
||||
if (row.id == 23 || row.id == 11) {
|
||||
row.left_pan = 1;
|
||||
row.right_pan = 0;
|
||||
}
|
||||
|
||||
// Auto-generate some basic keyword and question based on text
|
||||
if (row.text.includes('시위')) { row.keyword = '요원들의 시위 발생'; row.question = '시위를 어떻게 처리하시겠습니까?'; }
|
||||
else if (row.text.includes('격리실')) { row.keyword = '격리실 오염 발생'; row.question = '오염을 어떻게 정화하시겠습니까?'; }
|
||||
else if (row.text.includes('촉수')) { row.keyword = '촉수 괴물 출현'; row.question = '출동 승인하시겠습니까?'; }
|
||||
else if (row.text.includes('DNA')) { row.keyword = '변이 DNA 이식 제안'; row.question = '실험을 승인하시겠습니까?'; }
|
||||
else if (row.text.includes('폭격')) { row.keyword = '전술 폭격 건의'; row.question = '폭격을 승인하시겠습니까?'; }
|
||||
else if (row.text.includes('암시장')) { row.keyword = '부산물 암시장 매각'; row.question = '매각을 허가하시겠습니까?'; }
|
||||
else if (row.text.includes('대중 매체')) { row.keyword = '존재 양지화 프로젝트'; row.question = '프로젝트를 승인하시겠습니까?'; }
|
||||
else if (row.text.includes('도서관')) { row.keyword = '대규모 도서관 건립'; row.question = '예산을 배정하시겠습니까?'; }
|
||||
else if (row.text.includes('마녀사냥')) { row.keyword = '이단 색출 마녀사냥'; row.question = '어떻게 개입하시겠습니까?'; }
|
||||
else if (row.text.includes('유튜버')) { row.keyword = '변칙 현상 영상 유출'; row.question = '어떻게 처리하시겠습니까?'; }
|
||||
else if (row.text.includes('인신 공양')) { row.keyword = '사이비 종교 인신 공양'; row.question = '어떻게 대응하시겠습니까?'; }
|
||||
else if (row.text.includes('감사')) { row.keyword = '의회의 긴급 감사'; row.question = '어떻게 대처하시겠습니까?'; }
|
||||
else if (row.text.includes('초과 근무')) { row.keyword = '연구 부서 파업 조짐'; row.question = '요구를 수용하시겠습니까?'; }
|
||||
else if (row.text.includes('무역로')) { row.keyword = '이계 무역로 개방'; row.question = '무역로를 개방하시겠습니까?'; }
|
||||
else if (row.text.includes('비밀 금고')) { row.keyword = '전임 국장 비밀 금고'; row.question = '금고를 개방하시겠습니까?'; }
|
||||
else if (row.text.includes('메가코프')) { row.keyword = '메가코프 협박 작전'; row.question = '작전을 실행하시겠습니까?'; }
|
||||
else if (row.text.includes('시민')) { row.keyword = '민간인 구역 붕괴 위기'; row.question = '어떻게 명령하시겠습니까?'; }
|
||||
else if (row.text.includes('파업')) { row.keyword = '행정부 파업 예고'; row.question = '어떻게 대응하시겠습니까?'; }
|
||||
else if (row.text.includes('뇌를 결합')) { row.keyword = '불침번 요원 개발'; row.question = '배치하시겠습니까?'; }
|
||||
else if (row.text.includes('스파이')) { row.keyword = '내부 스파이 첩보'; row.question = '어떻게 조사하시겠습니까?'; }
|
||||
else if (row.text.includes('수용소')) { row.keyword = '변이 요원 수용소'; row.question = '건립을 승인하시겠습니까?'; }
|
||||
else if (row.text.includes('유물')) { row.keyword = '이계의 유물 인양'; row.question = '유물을 어떻게 처리하시겠습니까?'; }
|
||||
else if (row.text.includes('눈동자')) { row.keyword = '이계의 주시자 출현'; row.question = '대중을 어떻게 통제하시겠습니까?'; }
|
||||
else if (row.text.includes('트라우마')) { row.keyword = '요원 집단 트라우마'; row.question = '어떻게 지원하시겠습니까?'; }
|
||||
else if (row.text.includes('폭로')) { row.keyword = '내부 고발자 발생'; row.question = '어떻게 막으시겠습니까?'; }
|
||||
else if (row.text.includes('타이머')) { row.keyword = '차원 붕괴 폭탄 오작동'; row.question = '어떻게 대응하시겠습니까?'; }
|
||||
else if (row.text.includes('군사 기업')) { row.keyword = 'PMC 흡수 합병'; row.question = '결탁하시겠습니까?'; }
|
||||
else if (row.text.includes('관찰')) { row.keyword = '대규모 거주 구역 실험장화'; row.question = '제안을 승인하시겠습니까?'; }
|
||||
else if (row.text.includes('이계의 군주')) { row.keyword = '이계 군주의 외교'; row.question = '제안을 수락하시겠습니까?'; }
|
||||
else if (row.text.includes('믹스 커피')) { row.keyword = '믹스 커피 재고 고갈'; row.question = '어떻게 해결하시겠습니까?'; }
|
||||
else if (row.text.includes('신화적 존재')) { row.keyword = '신화적 존재 강림'; row.question = '어떻게 대적하시겠습니까?'; }
|
||||
else { row.keyword = '비정상적 위기 발생'; row.question = '국장님, 결단이 필요합니다.'; }
|
||||
});
|
||||
|
||||
const newCsv = Papa.unparse(parsed.data, {
|
||||
columns: [
|
||||
'id','type','text','flavor',
|
||||
'id','type','keyword','question','text','flavor',
|
||||
'left_text','left_res','left_ent','left_pan','left_rsk','left_bud','left_per','left_inf','left_wea','left_for','left_sur',
|
||||
'right_text','right_res','right_ent','right_pan','right_rsk','right_bud','right_per','right_inf','right_wea','right_for','right_sur'
|
||||
]
|
||||
});
|
||||
|
||||
fs.writeFileSync('./src/data/cards.csv', newCsv);
|
||||
console.log('CSV updated');
|
||||
console.log('CSV updated with keyword and question');
|
||||
|
||||
37
client/update_csv_tags.js
Normal file
37
client/update_csv_tags.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import fs from 'fs';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
const csv = fs.readFileSync('./src/data/cards.csv', 'utf-8');
|
||||
const parsed = Papa.parse(csv, { header: true });
|
||||
|
||||
parsed.data.forEach(row => {
|
||||
if (!row.id) return;
|
||||
|
||||
// Initialize empty tags
|
||||
row.left_tag = '';
|
||||
row.right_tag = '';
|
||||
|
||||
// Assign tags based on id or content
|
||||
if (row.id == 1) { row.left_tag = 'tag_ruthless'; } // 무력 진압
|
||||
if (row.id == 4) { row.left_tag = 'tag_madness'; } // DNA 이식 실험 승인
|
||||
if (row.id == 6) { row.left_tag = 'tag_corruption'; } // 부산물 암시장 매각
|
||||
if (row.id == 7) { row.left_tag = 'tag_coverup'; } // 대중 매체 장악 감시망
|
||||
if (row.id == 10) { row.left_tag = 'tag_coverup'; } // 유튜버 제거
|
||||
if (row.id == 11) { row.right_tag = 'tag_cult'; } // 종교 의식 방관
|
||||
if (row.id == 12) { row.left_tag = 'tag_corruption'; } // 장부 은폐 공작
|
||||
if (row.id == 14) { row.left_tag = 'tag_alien_contact'; } // 이계 무역로 개방
|
||||
if (row.id == 17) { row.left_tag = 'tag_ruthless'; } // 시민 포기 및 폭격
|
||||
if (row.id == 22) { row.left_tag = 'tag_alien_contact'; } // 유물 집중 연구
|
||||
if (row.id == 29) { row.left_tag = 'tag_alien_contact'; } // 이계 외교 사절 거래
|
||||
});
|
||||
|
||||
const newCsv = Papa.unparse(parsed.data, {
|
||||
columns: [
|
||||
'id','type','keyword','question','text','flavor',
|
||||
'left_text','left_tag','left_res','left_ent','left_pan','left_rsk','left_bud','left_per','left_inf','left_wea','left_for','left_sur',
|
||||
'right_text','right_tag','right_res','right_ent','right_pan','right_rsk','right_bud','right_per','right_inf','right_wea','right_for','right_sur'
|
||||
]
|
||||
});
|
||||
|
||||
fs.writeFileSync('./src/data/cards.csv', newCsv);
|
||||
console.log('CSV updated with tags');
|
||||
@@ -174,3 +174,32 @@
|
||||
- 태그 0~3개: 페이즈1 (사태 파악 및 은폐의 시작)
|
||||
- 태그 4~7개: 페이즈2 (프로젝트 가동 및 이사회의 개입 본격화)
|
||||
- 태그 8개 이상: 페이즈3 (통제 불능의 사태 및 종말의 카운트다운)
|
||||
|
||||
## 라운드 플로우차트 (Round Flowchart)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[라운드 시작] --> B(잔여 결재 건수 확인);
|
||||
B --> C{결재 건수 남음?};
|
||||
C -- Yes --> D[결재 단계];
|
||||
D --> E(토큰/파라미터 증감);
|
||||
E --> F{파산 상태인가?};
|
||||
F -- Yes --> G[파산 카운터 증가];
|
||||
F -- No --> H;
|
||||
G --> H(잔여 결재 1감소);
|
||||
H --> B;
|
||||
|
||||
C -- No --> I[정기 의회 단계 진입];
|
||||
I --> J{파산 문책 경고 상태인가?};
|
||||
J -- Yes (토큰 마이너스 상태) --> K[해임 / 게임 오버];
|
||||
J -- No --> L{파산 카운터 확인};
|
||||
L -- 1 이상 --> M[파산 문책 경고 및 패널티];
|
||||
L -- 0 --> N[정상 상정 안건 처리];
|
||||
M --> O;
|
||||
N --> O(의원 표결 및 국장 개입);
|
||||
O --> P{탄핵안 통과?};
|
||||
P -- Yes --> Q[정치적 탄핵 / 게임 오버];
|
||||
P -- No --> R[법안 결과 반영];
|
||||
R --> S(다음 라운드 진입 및 필요 결재 건수 재설정);
|
||||
S --> A;
|
||||
```
|
||||
|
||||
@@ -51,7 +51,15 @@
|
||||
- **내부 감사**: [인력] 소모. 임원 기권 처리. 페널티: 내부 저항 상승.
|
||||
|
||||
## 4. 콘텐츠 진행 및 페이징 (Progression)
|
||||
플레이어가 획득한 [내러티브 태그]의 스택에 따라 게임의 페이즈가 강제 전환됩니다.
|
||||
게임의 진행은 '시간의 흐름(결재 건수)'과 '스토리 진행(내러티브 태그)'이라는 두 가지 축을 기준으로 전개됩니다.
|
||||
|
||||
#### 4.1. 시간의 흐름과 라운드 시스템 (Time & Rounds)
|
||||
- **기초 단위**: 결재 1건 (플레이어의 카드 스와이프 1회).
|
||||
- **라운드 (Round)**: '제 X차 그림자 의회 회의'로 구분되는 주기 단위. 정해진 결재 건수(XX건)가 모일 때마다 하나의 라운드가 종료되며 의회가 개최됩니다.
|
||||
- **종료 조건**: 라운드가 계속 반복되며 의회의 압박을 방어하다가, 획득한 내러티브 태그가 8개 이상이 되어 종말 카운트다운 페이즈를 돌파하면 최종 엔딩이 트리거됩니다.
|
||||
|
||||
#### 4.2. 페이즈 전환과 덱 풀 변경 (Story Phases)
|
||||
플레이어가 획득한 [내러티브 태그]의 스택에 따라 게임의 페이즈가 강제 전환됩니다. 페이즈가 전환될 때마다 덱에서 등장하는 딜레마 카드의 풀(Pool)이 극적으로 변경되며, 이는 반복되는 로그라이크 루프에서 게임이 지루해지는 것을 방지하는 핵심 장치입니다.
|
||||
- **페이즈 1 (태그 0~3개)**: 사태 파악 단계. 일상/위기 딜레마 중심으로 토큰 수급과 덱 오염 빌드업.
|
||||
- **페이즈 2 (태그 4~7개)**: 본격화 단계. 작전/극비 프로젝트가 등장하여 엔진 구축 및 자산 획득의 기회 제공.
|
||||
- **페이즈 3 (태그 8개 이상)**: 종말 카운트다운. 극한의 오염 카드와 뉴스 속보가 쏟아지는 자원 고갈의 생존 구간.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
제공해주신 기획서를 바탕으로, 게임의 핵심 화면들을 텍스트로 구조화한 **텍스트 와이어프레임(Text Wireframe)** 문서를 작성했습니다. 모바일과 스팀 환경을 모두 고려하여, 중앙 집중형 레이아웃을 기준으로 UI 요소들의 배치를 정의했습니다.
|
||||
모바일과 스팀 환경을 모두 고려하여, 중앙 집중형 레이아웃을 기준으로 UI 요소들의 배치를 정의했습니다.
|
||||
|
||||
# 📱 텍스트 와이어프레임 (Text Wireframe)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
플레이어가 스와이프를 통해 딜레마를 해결하고 덱을 구축하는 핵심 플레이 화면입니다.
|
||||
|
||||
### [상단 고정 영역: 미니멀 상태창]
|
||||
- **좌상단 (내러티브):** `[📖 4/8]` (현재 획득한 내러티브 태그 숫자만 작게 표기. 터치 시 연대기 팝업).
|
||||
- **좌상단 (내러티브):** `[📖 4/8]` (현재 획득한 내러티브 태그 숫자만 작게 표기. 터치 시 연대기 팝업).우측 여백에 다가오는 의회의 압박감을 시각화하는 `[제 X차 정례회의 상정까지: 잔여 00건]` 텍스트를 나란히 배치합니다.
|
||||
- **중앙~우상단 (핵심 붕괴 파라미터):**
|
||||
- 바(Bar) 형태를 버리고, 파라미터별 아이콘과 5개의 점(Dot)으로 1~5단계를 심플하게 표기합니다.
|
||||
- `[저항 ✊] ⚫⚫⚪⚪⚪` | `[엔트로피 🌀] ⚫⚫⚫⚫⚪` | `[대중공황 😱] ⚫⚫⚫⚪⚪`
|
||||
|
||||
Reference in New Issue
Block a user