기획 문서에서 미구현한 시스템 구현
다음과 같은 주요 시스템들이 클라이언트 단에 추가되었습니다:
1. 대중 공황 (Mass Panic) 시스템
• 기존의 저항, 엔트로피, 위험 수용 계수 외에 '대중 공황' 파라미터를 추가했습니다.
• cards.csv 파일에 left_pan , right_pan 컬럼을 동적으로 업데이트하고, 일부 카드에 패널티/효과를 부여했습니다.
• 게임 메인 화면 상단에 😱 아이콘과 1~5단계 점(Dot) 위젯을 추가했습니다.
2. 주간 의회 (Weekly Council) 시스템
• 10턴 주기로 발동하는 CouncilScreen 컴포넌트를 신규 제작했습니다.
• 13명(상임 8명, 지부장 5명)의 위원회를 시각적으로 구현하고 위험 수용 계수에 따라 반대 확률이 증가하도록 로직을
작성했습니다.
• 3가지 개입 액션(매수, 장부 조작, 내부 감사)을 예산, 정보, 인력 토큰 소모를 통해 실행할 수 있도록 했습니다.
• 탄핵안 발의 및 법안(이득/표적) 상정 기능이 정상 작동합니다.
3. 무제한 인벤토리 및 자산 (Assets) 시스템
• 극비 카드를 완료했을 때 랜덤한 자산을 얻어 하단 인벤토리 슬롯에 시각적으로 누적되는 기초 로직을 붙였습니다.
• 스와이프 카드의 드래그를 양방향(XY)으로 허용하여, 극비 프로젝트의 경우 '위로 스와이프'하여 보류/버리기가
가능하도록 했습니다.
4. 연대기 (Chronicle) 시스템
• 좌상단 내러티브 태그 배지를 클릭하면 그동안 플레이어가 결재한 스토리와 선택(좌/우/위)을 기록한 ChroniclePopup
이 노출되도록 구현했습니다.
5. 멀티 엔딩 및 도감 시스템
• 단순한 게임 오버 화면을 대체하는 EndingScreen 컴포넌트를 제작했습니다.
• 4대 붕괴 조건(사임, 세계 멸망, 문명 붕괴, 정치적 탄핵) 중 어느 것을 달성했는지 판별하여 원인과 결과를 표시해
줍니다.
• 브라우저의 localStorage 를 이용해 수집된 엔딩이 도감에 영구적으로 기록되도록 처리했습니다.
This commit is contained in:
93
client/src/components/ChroniclePopup.css
Normal file
93
client/src/components/ChroniclePopup.css
Normal file
@@ -0,0 +1,93 @@
|
||||
.chronicle-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chronicle-popup {
|
||||
background: var(--color-surface);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
max-height: 80%;
|
||||
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);
|
||||
}
|
||||
|
||||
.chronicle-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.chronicle-header h2 {
|
||||
margin: 0;
|
||||
font-family: var(--font-doc-title);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chronicle-content {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chronicle-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chronicle-item {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.chronicle-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.turn-number {
|
||||
font-size: 12px;
|
||||
color: var(--color-token-info);
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chronicle-item p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #ddd;
|
||||
}
|
||||
29
client/src/components/ChroniclePopup.jsx
Normal file
29
client/src/components/ChroniclePopup.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import './ChroniclePopup.css';
|
||||
|
||||
export default function ChroniclePopup({ chronicle, onClose }) {
|
||||
return (
|
||||
<div className="chronicle-overlay" onClick={onClose}>
|
||||
<div className="chronicle-popup" onClick={e => e.stopPropagation()}>
|
||||
<div className="chronicle-header">
|
||||
<h2>국장의 연대기</h2>
|
||||
<button className="close-btn" onClick={onClose}>×</button>
|
||||
</div>
|
||||
<div className="chronicle-content">
|
||||
{chronicle.length === 0 ? (
|
||||
<p className="empty-text">아직 기록된 역사가 없습니다.</p>
|
||||
) : (
|
||||
<ul className="chronicle-list">
|
||||
{chronicle.map((entry, idx) => (
|
||||
<li key={idx} className="chronicle-item">
|
||||
<span className="turn-number">Turn {entry.turn}</span>
|
||||
<p>{entry.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
client/src/components/CouncilScreen.css
Normal file
150
client/src/components/CouncilScreen.css
Normal file
@@ -0,0 +1,150 @@
|
||||
.council-screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px 20px;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.council-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.council-header h2 {
|
||||
font-family: var(--font-doc-title);
|
||||
color: var(--color-risk-critical);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.risk-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
.risk-bar {
|
||||
width: 200px;
|
||||
height: 10px;
|
||||
background: #333;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.risk-fill {
|
||||
height: 100%;
|
||||
background: var(--color-risk-critical);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.warning-blink {
|
||||
color: #f87171;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
.members-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
margin: 0 auto 40px auto;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.member-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: #2a2a2a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.member-icon.board {
|
||||
border: 2px solid var(--color-risk-base);
|
||||
}
|
||||
|
||||
.member-icon.global {
|
||||
border: 2px solid var(--color-token-info);
|
||||
}
|
||||
|
||||
.member-badge {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
font-size: 16px;
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.council-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.fat-btn {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.fat-btn:hover:not(.disabled) {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.fat-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-proceed {
|
||||
background: var(--color-swipe-right);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-proceed:hover {
|
||||
background: #2e7d32;
|
||||
}
|
||||
|
||||
.council-result {
|
||||
text-align: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.council-result h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
168
client/src/components/CouncilScreen.jsx
Normal file
168
client/src/components/CouncilScreen.jsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './CouncilScreen.css';
|
||||
|
||||
export default function CouncilScreen({ params, setParams, tokens, setTokens, tags, setTags, onResolve }) {
|
||||
const [members, setMembers] = useState([]);
|
||||
const [agenda, setAgenda] = useState('');
|
||||
const [impeachment, setImpeachment] = useState(false);
|
||||
const [actionTaken, setActionTaken] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize 13 members (8 board, 5 global)
|
||||
// Their vote depends on risk parameter and randomness
|
||||
const newMembers = Array.from({ length: 13 }).map((_, i) => {
|
||||
const type = i < 8 ? 'board' : 'global';
|
||||
// High risk = more likely to vote against
|
||||
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]);
|
||||
|
||||
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) {
|
||||
onResolve({ impeachment: false });
|
||||
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) }));
|
||||
}
|
||||
}
|
||||
onResolve({ impeachment: 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>
|
||||
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
44
client/src/components/EndingScreen.css
Normal file
44
client/src/components/EndingScreen.css
Normal file
@@ -0,0 +1,44 @@
|
||||
.ending-screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.ending-content {
|
||||
margin: 40px 0;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.ending-desc {
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
font-family: var(--font-doc-title);
|
||||
}
|
||||
|
||||
.ending-reason {
|
||||
font-size: 14px;
|
||||
color: #f87171;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ending-stats {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
}
|
||||
56
client/src/components/EndingScreen.jsx
Normal file
56
client/src/components/EndingScreen.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import './EndingScreen.css';
|
||||
|
||||
export default function EndingScreen({ params, impeachment, narrativeTags, onRestart }) {
|
||||
const [endingInfo, setEndingInfo] = useState({ title: '', desc: '', reason: '' });
|
||||
|
||||
useEffect(() => {
|
||||
// Determine ending
|
||||
let title = '미확인 종말';
|
||||
let desc = '기록되지 않은 파멸입니다.';
|
||||
let reason = '알 수 없는 원인';
|
||||
|
||||
if (impeachment) {
|
||||
title = '정치적 탄핵 (윗선 붕괴)';
|
||||
desc = '그림자 위원회가 당신의 권한을 박탈했습니다. 당신의 모든 업적은 지워질 것입니다.';
|
||||
reason = '주간 의회에서 적대적 파벌 임원 9명 이상 찬성';
|
||||
} else if (params.panic >= 5) {
|
||||
title = '문명 붕괴 (사회 붕괴)';
|
||||
desc = '대중은 광기에 휩싸였고, 문명은 스스로를 파괴했습니다. 통제 불능의 시대가 도래했습니다.';
|
||||
reason = '대중 공황 파라미터 5단계 도달';
|
||||
} else if (params.entropy >= 5) {
|
||||
title = '세계 멸망 (외부 붕괴)';
|
||||
desc = '장막이 찢어지고 이계의 존재들이 쏟아져 들어옵니다. 우리가 알던 세계는 끝났습니다.';
|
||||
reason = '엔트로피 파라미터 5단계 도달';
|
||||
} else if (params.resistance >= 5) {
|
||||
title = '사임 (내부 붕괴)';
|
||||
desc = '조직원들의 불만이 폭발하여 폭동이 일어났습니다. 당신은 자리에서 끌려 내려왔습니다.';
|
||||
reason = '내부 저항 파라미터 5단계 도달';
|
||||
}
|
||||
|
||||
setEndingInfo({ title, desc, reason });
|
||||
|
||||
// Save to collection
|
||||
const collection = JSON.parse(localStorage.getItem('ss_endings') || '[]');
|
||||
if (!collection.includes(title)) {
|
||||
collection.push(title);
|
||||
localStorage.setItem('ss_endings', JSON.stringify(collection));
|
||||
}
|
||||
}, [params, impeachment]);
|
||||
|
||||
return (
|
||||
<div className="ending-screen">
|
||||
<h1 className="glitch" data-text={endingInfo.title}>{endingInfo.title}</h1>
|
||||
<div className="ending-content">
|
||||
<p className="ending-desc">{endingInfo.desc}</p>
|
||||
<div className="ending-reason">
|
||||
<strong>발동 원인:</strong> {endingInfo.reason}
|
||||
</div>
|
||||
<div className="ending-stats">
|
||||
<span>획득한 내러티브 태그: {narrativeTags}개</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="restart-btn" onClick={onRestart}>도감에 기록하기 & 다음 회차 시작</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,12 @@
|
||||
}
|
||||
|
||||
.game-screen.phase-2 {
|
||||
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.8);
|
||||
justify-content: center;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.game-screen.phase-3 {
|
||||
@@ -147,6 +152,9 @@
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Game Over */
|
||||
|
||||
@@ -3,10 +3,13 @@ import Papa from 'papaparse';
|
||||
import cardsCsvRaw from '../data/cards.csv?raw';
|
||||
import { BookOpen, HandCoins, Droplet, Eye, Shield, Building2, EyeOff } from 'lucide-react';
|
||||
import SwipeCard from './SwipeCard';
|
||||
import CouncilScreen from './CouncilScreen';
|
||||
import EndingScreen from './EndingScreen';
|
||||
import ChroniclePopup from './ChroniclePopup';
|
||||
import './GameScreen.css';
|
||||
|
||||
export function GameScreen() {
|
||||
const [params, setParams] = useState({ resistance: 0, entropy: 0, risk: 0 });
|
||||
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);
|
||||
@@ -14,6 +17,13 @@ export function GameScreen() {
|
||||
const [gameOver, setGameOver] = useState(false);
|
||||
const [cards, setCards] = useState([]);
|
||||
const [currentCardIndex, setCurrentCardIndex] = useState(0);
|
||||
|
||||
const [turns, setTurns] = useState(0);
|
||||
const [showCouncil, setShowCouncil] = useState(false);
|
||||
const [impeachmentPassed, setImpeachmentPassed] = useState(false);
|
||||
const [chronicle, setChronicle] = useState([]);
|
||||
const [showChronicle, setShowChronicle] = useState(false);
|
||||
const [assets, setAssets] = useState([]);
|
||||
|
||||
// CSV Load Effect
|
||||
useEffect(() => {
|
||||
@@ -29,6 +39,7 @@ export function GameScreen() {
|
||||
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,
|
||||
@@ -41,6 +52,7 @@ export function GameScreen() {
|
||||
setParams(p => ({
|
||||
resistance: Math.max(0, p.resistance + (row.left_res || 0)),
|
||||
entropy: Math.max(0, p.entropy + (row.left_ent || 0)),
|
||||
panic: Math.max(0, p.panic + (row.left_pan || 0)),
|
||||
risk: Math.max(0, p.risk + (row.left_rsk || 0)),
|
||||
}));
|
||||
setTokens(t => ({
|
||||
@@ -60,6 +72,7 @@ export function GameScreen() {
|
||||
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,
|
||||
@@ -72,6 +85,7 @@ export function GameScreen() {
|
||||
setParams(p => ({
|
||||
resistance: Math.max(0, p.resistance + (row.right_res || 0)),
|
||||
entropy: Math.max(0, p.entropy + (row.right_ent || 0)),
|
||||
panic: Math.max(0, p.panic + (row.right_pan || 0)),
|
||||
risk: Math.max(0, p.risk + (row.right_rsk || 0)),
|
||||
}));
|
||||
setTokens(t => ({
|
||||
@@ -92,7 +106,7 @@ export function GameScreen() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.resistance >= 5 || params.entropy >= 5 || params.risk >= 5) {
|
||||
if (params.resistance >= 5 || params.entropy >= 5 || params.panic >= 5 || params.risk >= 5) {
|
||||
setGameOver(true);
|
||||
}
|
||||
if (narrativeTags >= 4 && narrativeTags < 8) setPhase(2);
|
||||
@@ -102,13 +116,47 @@ export function GameScreen() {
|
||||
const handleSwipe = (direction) => {
|
||||
if (cards.length === 0) return;
|
||||
const card = cards[currentCardIndex % cards.length];
|
||||
|
||||
let outcomeText = '';
|
||||
|
||||
if (direction === 'left' && card.left) {
|
||||
card.left.effect();
|
||||
outcomeText = card.left.text;
|
||||
} else if (direction === 'right' && card.right) {
|
||||
card.right.effect();
|
||||
outcomeText = card.right.text;
|
||||
} else if (direction === 'up') {
|
||||
outcomeText = '보류 / 버리기';
|
||||
}
|
||||
|
||||
if (outcomeText) {
|
||||
setChronicle(prev => [...prev, { turn: turns + 1, text: `[${card.type}] ${card.text} -> 결정: ${outcomeText}` }]);
|
||||
}
|
||||
|
||||
// Add random asset for '극비' success (just for demonstration)
|
||||
if (card.type === '극비' && direction !== 'up') {
|
||||
if (assets.length < 4) {
|
||||
setAssets(prev => [...prev, { id: Date.now(), icon: ['📦','💎','🧪','💾'][Math.floor(Math.random()*4)] }]);
|
||||
}
|
||||
}
|
||||
|
||||
setNarrativeTags(n => n + 1);
|
||||
setCurrentCardIndex(idx => idx + 1);
|
||||
|
||||
const newTurns = turns + 1;
|
||||
setTurns(newTurns);
|
||||
|
||||
if (newTurns > 0 && newTurns % 10 === 0) {
|
||||
setShowCouncil(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCouncilResolve = (result) => {
|
||||
setShowCouncil(false);
|
||||
if (result.impeachment) {
|
||||
setImpeachmentPassed(true);
|
||||
setGameOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDots = (value, max = 5) => {
|
||||
@@ -119,11 +167,23 @@ export function GameScreen() {
|
||||
|
||||
if (gameOver) {
|
||||
return (
|
||||
<div className="game-over">
|
||||
<h1 className="glitch" data-text="시스템 붕괴">시스템 붕괴</h1>
|
||||
<p>프로젝트가 파멸을 맞이했습니다. 임무 실패.</p>
|
||||
<button className="restart-btn" onClick={() => window.location.reload()}>다음 회차 시작</button>
|
||||
</div>
|
||||
<EndingScreen
|
||||
params={params}
|
||||
impeachment={impeachmentPassed}
|
||||
narrativeTags={narrativeTags}
|
||||
onRestart={() => window.location.reload()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showCouncil) {
|
||||
return (
|
||||
<CouncilScreen
|
||||
params={params} setParams={setParams}
|
||||
tokens={tokens} setTokens={setTokens}
|
||||
tags={tags} setTags={setTags}
|
||||
onResolve={handleCouncilResolve}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,7 +197,7 @@ export function GameScreen() {
|
||||
<div className={`game-screen ${phaseClass}`}>
|
||||
{/* Top Bar */}
|
||||
<div className="top-bar">
|
||||
<div className="narrative-badge">
|
||||
<div className="narrative-badge" onClick={() => setShowChronicle(true)} style={{cursor: 'pointer'}}>
|
||||
<BookOpen size={16} /> <span>{narrativeTags}/8</span>
|
||||
</div>
|
||||
<div className="parameters">
|
||||
@@ -149,6 +209,10 @@ export function GameScreen() {
|
||||
<span className="icon">🌀</span>
|
||||
<div className="dots">{renderDots(params.entropy)}</div>
|
||||
</div>
|
||||
<div className={`param ${params.panic >= 4 ? 'critical' : ''}`} style={{'--param-color': 'var(--color-panic-critical)'}}>
|
||||
<span className="icon">😱</span>
|
||||
<div className="dots">{renderDots(params.panic)}</div>
|
||||
</div>
|
||||
<div className={`param ${params.risk >= 4 ? 'critical' : ''}`} style={{'--param-color': 'var(--color-risk-critical)'}}>
|
||||
<span className="icon">⚠️</span>
|
||||
<div className="dots">{renderDots(params.risk)}</div>
|
||||
@@ -178,12 +242,20 @@ export function GameScreen() {
|
||||
<div className="tag"><EyeOff size={14} /> <span>{tags.surveillance}</span></div>
|
||||
</div>
|
||||
<div className="asset-slots">
|
||||
<div className="asset-slot"></div>
|
||||
<div className="asset-slot"></div>
|
||||
<div className="asset-slot"></div>
|
||||
<div className="asset-slot"></div>
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<div key={i} className="asset-slot">
|
||||
{assets[i] ? <span className="asset-icon">{assets[i].icon}</span> : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showChronicle && (
|
||||
<ChroniclePopup
|
||||
chronicle={chronicle}
|
||||
onClose={() => setShowChronicle(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,13 @@
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
.up-intent {
|
||||
color: var(--color-swipe-up);
|
||||
top: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.swipe-intent .intent-title {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 8px 16px;
|
||||
|
||||
@@ -9,6 +9,7 @@ const renderStats = (stats) => {
|
||||
|
||||
if (stats.res !== 0) items.push({ icon: '✊', label: '저항', val: stats.res, isDanger: true });
|
||||
if (stats.ent !== 0) items.push({ icon: '🌀', label: '엔트로피', val: stats.ent, isDanger: true });
|
||||
if (stats.pan !== 0) items.push({ icon: '😱', label: '공황', val: stats.pan, isDanger: true });
|
||||
if (stats.rsk !== 0) items.push({ icon: '⚠️', label: '위험', val: stats.rsk, isDanger: true });
|
||||
|
||||
if (stats.bud !== 0) items.push({ icon: <HandCoins size={14} />, label: '예산', val: stats.bud });
|
||||
@@ -41,6 +42,7 @@ const renderStats = (stats) => {
|
||||
|
||||
export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
const x = useMotionValue(0);
|
||||
const y = useMotionValue(0);
|
||||
const controls = useAnimation();
|
||||
|
||||
// Dynamic friction based on phase
|
||||
@@ -50,6 +52,7 @@ export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
const rotate = useTransform(x, [-200, 200], [-15 * friction, 15 * friction]);
|
||||
const leftOpacity = useTransform(x, [-100, -50, 0], [1, 0.5, 0]);
|
||||
const rightOpacity = useTransform(x, [0, 50, 100], [0, 0.5, 1]);
|
||||
const upOpacity = useTransform(y, [-100, -50, 0], [1, 0.5, 0]);
|
||||
|
||||
const [exitX, setExitX] = useState(0);
|
||||
|
||||
@@ -57,19 +60,21 @@ export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
useEffect(() => {
|
||||
setExitX(0);
|
||||
x.set(0);
|
||||
controls.start({ x: 0, opacity: 1, rotate: 0, transition: { duration: 0 } });
|
||||
}, [card, controls, x]);
|
||||
y.set(0);
|
||||
controls.start({ x: 0, y: 0, opacity: 1, rotate: 0, transition: { duration: 0 } });
|
||||
}, [card, controls, x, y]);
|
||||
|
||||
const handleDragEnd = async (event, info) => {
|
||||
const threshold = 100;
|
||||
if (info.offset.x > threshold) {
|
||||
setExitX(300);
|
||||
await controls.start({ x: 300, opacity: 0, transition: { duration: 0.3 } });
|
||||
onSwipe('right');
|
||||
} else if (info.offset.x < -threshold) {
|
||||
setExitX(-300);
|
||||
await controls.start({ x: -300, opacity: 0, transition: { duration: 0.3 } });
|
||||
onSwipe('left');
|
||||
} else if (info.offset.y < -threshold && card.type === '극비') {
|
||||
await controls.start({ y: -300, opacity: 0, transition: { duration: 0.3 } });
|
||||
onSwipe('up');
|
||||
} else {
|
||||
// Snap back
|
||||
controls.start({ x: 0, y: 0, transition: { type: 'spring', stiffness: 300, damping: 20 } });
|
||||
@@ -80,9 +85,9 @@ export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
<div className="card-container">
|
||||
<motion.div
|
||||
className="swipe-card"
|
||||
drag="x"
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
style={{ x, rotate }}
|
||||
drag
|
||||
dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
|
||||
style={{ x, y, rotate }}
|
||||
onDragEnd={handleDragEnd}
|
||||
animate={controls}
|
||||
whileTap={{ scale: 0.98, cursor: 'grabbing' }}
|
||||
@@ -110,6 +115,11 @@ export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
<span className="intent-title">{card.right?.text}</span>
|
||||
{renderStats(card.right?.stats)}
|
||||
</motion.div>
|
||||
{card.type === '극비' && (
|
||||
<motion.div className="swipe-intent up-intent" style={{ opacity: upOpacity }}>
|
||||
<span className="intent-title">보류 / 버리기</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
id,type,text,flavor,left_text,left_res,left_ent,left_rsk,left_bud,left_per,left_inf,left_wea,left_for,left_sur,right_text,right_res,right_ent,right_rsk,right_bud,right_per,right_inf,right_wea,right_for,right_sur
|
||||
1,일상,B구역 하급 요원들이 예산 삭감에 반발해 장비 반납 시위를 하고 있습니다.,우리가 세상을 구하는데 월급이 깎인다고요? 촉수에 맞아 죽으나 굶어 죽으나 똑같습니다!,무력 진압,1,0,0,0,-10,0,0,0,0,요구 수용,0,0,0,-20,0,0,0,0,0
|
||||
2,위기,지하 4층 격리실의 문은 닫혀 있습니다만 요원 12명이 남긴 체액의 총량이 인간 12명의 부피를 초과했습니다.,보고서 첨부: 액체화된 직원은 산재 처리가 안 됩니다.,네이팜 소각 강행,0,0,0,0,-30,0,0,0,0,조사팀 파견,0,1,1,0,0,0,0,0,0
|
||||
3,일상,7구역에 촉수 괴물이 출현했습니다. 작전팀이 법인카드 영수증 제출을 누락해서 출동 승인이 반려된 상태입니다.,규정은 규정입니다. 영수증 없이 세계를 구하는 건 용납할 수 없습니다.,내역 위조 후 출동,0,0,1,0,0,-10,0,0,0,규정대로 반려,1,1,0,0,0,0,0,0,0
|
||||
4,극비,진화 미래당이 제안합니다. 요원들에게 괴수의 DNA를 소량 이식하면 작전 효율이 300% 상승합니다. 예산은 우리가 대죠.,인간의 한계는 명확합니다. 조금 징그러워지는 것쯤은 감수해야죠.,실험 승인,0,2,0,40,40,0,0,0,0,제안 거절,1,0,0,0,0,0,0,0,0
|
||||
5,작전,철의 수호당이 민간인 구역에 전술 폭격을 가해 S급 변칙 개체를 제거하자고 건의했습니다.,희생 없는 승리는 없습니다. 소를 위해 대를... 아니 대를 위해 소를 희생하죠.,궤도 폭격 승인,0,0,2,0,0,0,0,2,0,작전 기각,1,0,0,0,0,0,0,0,0
|
||||
6,일상,차원 무역당 소속 브로커가 처리된 괴수의 부산물을 암시장에 팔아치우자고 속삭입니다.,이 이빨 하나면 국장님 연봉의 10배입니다. 눈 한 번만 딱 감으시죠.,매각 허가,0,0,1,30,0,0,0,0,0,원칙대로 소각,0,0,0,-10,0,0,0,0,0
|
||||
7,작전,대중 매체를 장악하여 I.S.C.O의 존재를 서서히 양지화하는 프로젝트입니다. 막대한 정보 토큰이 소모됩니다.,우리가 이 행성의 진짜 지배자임을 알릴 때가 되었습니다.,감시망 구축,0,0,0,0,0,-40,0,0,2,프로젝트 보류,1,0,0,0,0,0,0,0,0
|
||||
8,극비,고대 언어 해독을 위해 대규모 도서관 건립이 필요합니다. 지식 보존당이 학자들을 동원할 것입니다.,이해할 수 없는 것을 죽이는 건 야만인이나 하는 짓입니다.,예산 배정,0,0,0,-40,0,40,0,0,0,예산 전액 삭감,1,0,1,0,0,0,0,0,0
|
||||
9,위기,기술 원리주의자들이 마녀사냥을 시작했습니다. 기구 내 마법 사용 요원들을 이단으로 규정하고 색출 중입니다.,더러운 마법과 변이에 의존해선 안 됩니다. 오직 순수한 인간의 힘으로!,적당히 묵인한다,1,0,0,0,-30,0,0,0,0,군사력으로 제압,0,0,1,0,0,-20,0,0,0
|
||||
10,일상,한 유튜버가 변칙 현상을 촬영해 업로드했습니다. 침묵의 장막당이 즉각적인 물리적 제거와 기억 소거를 요구합니다.,대중은 진실을 감당할 지능이 없습니다. 무덤까지 비밀을 덮으십시오.,제거 절차 승인,0,0,-1,0,0,-20,0,0,0,영상만 삭제,0,1,0,0,0,0,0,0,0
|
||||
11,위기,특정 구역의 사이비 종교가 고대 신을 소환하기 위해 시민 100명을 인신 공양하려 합니다. 공존당은 이를 방관하라 합니다.,위대한 그분들이 배가 고프다 하십니다. 조금만 양보하면 평화롭습니다.,군사 개입 지시,1,0,0,0,-40,0,0,0,0,의식 방관,0,2,0,0,0,0,0,0,0
|
||||
12,위기,의회에서 국장의 예산 전용 의혹을 제기하며 긴급 감사를 통보했습니다. 장부를 조작해야 합니다!,세상을 구하는 데 돈 좀 쓴 게 죄입니까? 빨리 덮을 정보팀을 부르십시오.,은폐 공작 실행,0,0,1,0,0,-40,0,0,0,횡령 사실 인정,0,0,2,0,0,0,0,0,0
|
||||
13,일상,연구 부서에서 초과 근무로 인한 집단 피로를 호소하며 휴가와 특별 보너스를 요구하고 있습니다.,저희도 사람입니다. 괴수 해부하다가 과로사하긴 싫어요.,특별 보너스 지급,-1,0,0,-30,0,0,0,0,0,요구 전면 묵살,1,0,0,0,0,0,0,0,0
|
||||
14,작전,이계와의 무역로를 임시 개방하여 자원을 조달하자는 안건이 상정되었습니다. 미지의 오염이 우려됩니다.,저 너머의 세계엔 우리가 상상도 못 할 자원이 널려있습니다. 황금알을 낳는 거위죠.,게이트 강제 개방,0,1,0,50,0,0,0,0,0,폐쇄 상태 유지,1,0,0,0,0,0,0,0,0
|
||||
15,극비,전임 국장이 남긴 비밀 금고를 발견했습니다. 여는 데 특수 장비와 전문 인력이 필요합니다.,전임 국장님이 숙청당하기 전 남긴 비자금 장부일지도 모릅니다.,강제 개방 지시,0,0,0,20,-30,0,1,0,0,영구 봉인,0,0,0,0,0,0,0,0,0
|
||||
16,작전,글로벌 메가코프를 협박해 I.S.C.O의 비자금을 세탁하고 대규모 자금을 확보할 기회입니다.,그들이 우리 덕분에 안전하게 사업하는 겁니다. 보호세 좀 걷는 게 뭐 어떻습니까.,작전 실행,0,0,0,0,0,-30,2,0,0,작전 취소,1,0,0,0,0,0,0,0,0
|
||||
17,위기,제7구역이 S급 변칙 개체에 의해 완전히 붕괴될 위기입니다. 민간인을 포기하고 융단 폭격을 해야 할까요?,시민 10만 명 구출 비용보다 유가족 합의금이 예산을 15% 더 아낄 수 있습니다.,시민 포기 및 폭격,0,-1,2,0,0,0,0,0,0,결사 방어 명령,0,0,0,-40,-40,0,0,0,0
|
||||
18,일상,새로운 회식비 규정이 너무 짜다며 행정부에서 파업을 예고했습니다. 괴수 사체 처리 지연으로 악취가 진동합니다.,기구의 절반은 서류 작업으로 돌아갑니다. 그들을 화나게 해선 안 됩니다.,예산 전용으로 무마,0,0,1,-20,0,0,0,0,0,파업 강경 대응,1,0,0,0,0,0,0,0,0
|
||||
19,극비,비밀 연구소에서 인공지능과 죽은 요원의 뇌를 결합한 불침번 요원을 개발했습니다.,잠도 안 자고 월급도 안 받습니다. 윤리적 문제만 빼면 완벽한 자산이죠.,실전 즉각 배치,0,1,0,0,0,20,0,1,0,즉각 폐기 처분,1,0,0,-10,0,0,0,0,0
|
||||
20,일상,기구 내부에 스파이가 있다는 첩보가 들어왔습니다. 대대적인 사상 검증이 필요할지도 모릅니다.,누군가 정보를 빼돌려 의회에 넘기고 있습니다. 잡히면 믹서기에 넣어야 합니다.,대규모 사찰 시작,2,0,0,0,0,30,0,0,0,증거 부족으로 덮음,0,0,1,0,0,0,0,0,0
|
||||
21,작전,순수 인간당이 변이의 기미가 보이는 요원들을 격리 수용할 대규모 시설 건립을 제안했습니다.,괴물과 싸우다 보면 괴물이 되는 법. 그들이 선을 넘기 전에 가둬야 합니다.,수용소 건립 승인,0,0,0,-40,0,0,0,1,0,제안 단호히 거절,1,0,1,0,0,0,0,0,0
|
||||
22,극비,고대 공존당의 사제들이 심해의 유물을 인양했습니다. 만지는 자의 정신을 갉아먹지만 막대한 지식을 줍니다.,약간의 두통만 참으면 이계의 비밀을 모두 알 수 있습니다. 요원 하나쯤 미쳐도 괜찮습니다.,유물 집중 연구,0,1,0,0,0,50,0,0,0,유물 파괴 명령,1,0,1,0,0,0,0,0,0
|
||||
23,위기,얇아진 장막 너머로 거대한 눈동자가 며칠째 지구를 주시하고 있습니다. 대중의 공포가 확산되고 있습니다.,하늘을 보지 마십시오. 아니 차라리 눈을 감고 살게 하십시오.,인터넷 전면 차단,2,0,0,0,0,0,0,0,2,진정제 무상 배포,0,0,0,-50,0,0,0,0,0
|
||||
24,일상,요원들이 괴수 진압 후 심각한 트라우마에 시달리고 있습니다. 심리 치료 지원이 시급합니다.,밤마다 촉수가 목을 조르는 악몽을 꾼답니다. 차라리 그 부분 기억을 지워버릴까요?,정신과 치료 지원,0,0,0,-20,10,0,0,0,0,트라우마 기억 소거,0,1,1,0,0,0,0,0,0
|
||||
25,극비,의회 의원 중 한 명이 기구의 비리를 폭로하려 합니다. 침묵의 장막당이 사고사 처리를 제안합니다.,국장님 계단에서 구르거나 심장마비가 오는 건 아주 흔한 자연의 섭리입니다.,사고사 위장 지시,0,0,-1,0,0,-30,0,0,0,폭로전 정면 돌파,0,0,2,0,0,0,0,0,0
|
||||
26,위기,차원 붕괴 폭탄의 타이머가 오작동을 일으켰습니다! 당장 끄지 않으면 7구역이 통째로 증발합니다.,최저가 입찰로 부품을 사서 폭탄을 만든 결과입니다. 누군가는 책임져야 합니다.,특수 해체팀 투입,0,0,0,0,-40,0,0,0,0,폭발 방관 및 대피,0,2,0,0,0,0,0,0,0
|
||||
27,작전,군부와 결탁하여 민간 군사 기업(PMC)을 I.S.C.O의 통제 하에 두려는 작전입니다.,돈으로 충성심을 사는 건 가장 안전한 방법입니다. 물론 돈이 많을 때 이야기죠.,결탁 및 흡수 합병,0,0,0,-40,0,0,0,2,0,독자적 노선 유지,1,0,0,0,0,0,0,0,0
|
||||
28,작전,변칙 개체 관찰을 위해 1구역을 돔으로 덮고 거대한 실험장으로 만들자는 제안이 올라왔습니다.,시민들이 좀 불편해하겠지만 과학적 진보를 위해 그 정도는 감수해야죠.,돔 건설 강행,0,1,0,-50,0,0,0,0,0,윤리적 이유로 기각,0,0,1,0,0,0,0,0,0
|
||||
29,극비,이계의 군주가 외교 사절을 보내왔습니다. 요원 10명을 노예로 주면 최고급 이계 무기를 주겠다고 합니다.,요원 10명의 생명 가치보다 이계 무기가 가져올 파급력이 월등히 높습니다.,거래 즉시 성사,0,0,0,0,-40,0,0,2,0,사절단 전원 사살,0,1,1,0,0,0,0,0,0
|
||||
30,일상,국장님 믹스 커피가 다 떨어졌습니다. 이게 무슨 세상의 종말보다 더 큰 위기인 줄 아십니까? 행정부서의 불만이 폭주합니다.,종말을 결재하기 전엔 커피 한 잔의 여유가 필요한 법입니다. 안 그렇습니까?,최고급 원두 지원,-1,0,0,-10,0,0,0,0,0,헛소리 금지령,1,0,0,0,0,0,0,0,0
|
||||
31,위기,신화적 존재가 깨어나려 합니다. 하늘이 피처럼 붉게 물들었고 요원들은 미쳐가고 있습니다!,최종 결재의 시간입니다. 세상의 물리 법칙을 비틀 것입니까 아니면 피로 막을 것입니까?,현실 조작기 가동,0,2,0,0,0,-50,0,0,0,모든 예비병력 투입,0,0,1,0,-60,0,0,0,0
|
||||
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
|
||||
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
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
@@ -8,6 +8,9 @@
|
||||
--color-entropy-base: #5C3A21;
|
||||
--color-entropy-critical: #8B0000;
|
||||
|
||||
--color-panic-base: #4A148C;
|
||||
--color-panic-critical: #9C27B0;
|
||||
|
||||
--color-resistance-base: #2F4F4F;
|
||||
--color-resistance-critical: #00FFCC;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user