From c0921548ec6cf7f98a4a6b7b29801ded377906fc Mon Sep 17 00:00:00 2001 From: Kyoung5seo Date: Wed, 17 Jun 2026 23:14:15 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B8=B0=ED=9A=8D=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=AF=B8=EA=B5=AC=ED=98=84=ED=95=9C=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84=20=20=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=8C=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EC=A3=BC?= =?UTF-8?q?=EC=9A=94=20=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=93=A4=EC=9D=B4=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EB=8B=A8?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80=EB=90=98=EC=97=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4:=20=20=201.=20=EB=8C=80=EC=A4=91=20=EA=B3=B5?= =?UTF-8?q?=ED=99=A9=20(Mass=20Panic)=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=20?= =?UTF-8?q?=20=20=20=20=20=E2=80=A2=20=EA=B8=B0=EC=A1=B4=EC=9D=98=20?= =?UTF-8?q?=EC=A0=80=ED=95=AD,=20=EC=97=94=ED=8A=B8=EB=A1=9C=ED=94=BC,=20?= =?UTF-8?q?=EC=9C=84=ED=97=98=20=EC=88=98=EC=9A=A9=20=EA=B3=84=EC=88=98=20?= =?UTF-8?q?=EC=99=B8=EC=97=90=20'=EB=8C=80=EC=A4=91=20=EA=B3=B5=ED=99=A9'?= =?UTF-8?q?=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=20=20=20=20?= =?UTF-8?q?=20=20=E2=80=A2=20=20cards.csv=20=20=ED=8C=8C=EC=9D=BC=EC=97=90?= =?UTF-8?q?=20=20left=5Fpan=20,=20=20right=5Fpan=20=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=EC=9D=84=20=EB=8F=99=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=ED=95=98=EA=B3=A0,=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=20=EC=B9=B4=EB=93=9C=EC=97=90=20=ED=8C=A8=EB=84=90?= =?UTF-8?q?=ED=8B=B0/=ED=9A=A8=EA=B3=BC=EB=A5=BC=20=EB=B6=80=EC=97=AC?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=20=20=20=20=20=20?= =?UTF-8?q?=E2=80=A2=20=EA=B2=8C=EC=9E=84=20=EB=A9=94=EC=9D=B8=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=83=81=EB=8B=A8=EC=97=90=20=F0=9F=98=B1=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=EA=B3=BC=201~5=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=A0=90(Dot)=20=EC=9C=84=EC=A0=AF=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=20=202.=20?= =?UTF-8?q?=EC=A3=BC=EA=B0=84=20=EC=9D=98=ED=9A=8C=20(Weekly=20Council)=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=20=20=20=20=20=20=E2=80=A2=2010?= =?UTF-8?q?=ED=84=B4=20=EC=A3=BC=EA=B8=B0=EB=A1=9C=20=EB=B0=9C=EB=8F=99?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=20CouncilScreen=20=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EC=8B=A0=EA=B7=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=91=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=20=20=20=20?= =?UTF-8?q?=20=20=E2=80=A2=2013=EB=AA=85(=EC=83=81=EC=9E=84=208=EB=AA=85,?= =?UTF-8?q?=20=EC=A7=80=EB=B6=80=EC=9E=A5=205=EB=AA=85)=EC=9D=98=20?= =?UTF-8?q?=EC=9C=84=EC=9B=90=ED=9A=8C=EB=A5=BC=20=EC=8B=9C=EA=B0=81?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=ED=98=84=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=9C=84=ED=97=98=20=EC=88=98=EC=9A=A9=20=EA=B3=84?= =?UTF-8?q?=EC=88=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B0=98=EB=8C=80=20?= =?UTF-8?q?=ED=99=95=EB=A5=A0=EC=9D=B4=20=EC=A6=9D=EA=B0=80=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=20=20=20=20?= =?UTF-8?q?=20=20=EC=9E=91=EC=84=B1=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= =?UTF-8?q?=20=20=20=20=20=20=20=E2=80=A2=203=EA=B0=80=EC=A7=80=20?= =?UTF-8?q?=EA=B0=9C=EC=9E=85=20=EC=95=A1=EC=85=98(=EB=A7=A4=EC=88=98,=20?= =?UTF-8?q?=EC=9E=A5=EB=B6=80=20=EC=A1=B0=EC=9E=91,=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20=EA=B0=90=EC=82=AC)=EC=9D=84=20=EC=98=88=EC=82=B0,=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4,=20=EC=9D=B8=EB=A0=A5=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=86=8C=EB=AA=A8=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=20=20=20=20=20=20?= =?UTF-8?q?=E2=80=A2=20=ED=83=84=ED=95=B5=EC=95=88=20=EB=B0=9C=EC=9D=98=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B2=95=EC=95=88(=EC=9D=B4=EB=93=9D/=ED=91=9C?= =?UTF-8?q?=EC=A0=81)=20=EC=83=81=EC=A0=95=20=EA=B8=B0=EB=8A=A5=EC=9D=B4?= =?UTF-8?q?=20=EC=A0=95=EC=83=81=20=EC=9E=91=EB=8F=99=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20=20=203.=20=EB=AC=B4=EC=A0=9C=ED=95=9C=20=EC=9D=B8?= =?UTF-8?q?=EB=B2=A4=ED=86=A0=EB=A6=AC=20=EB=B0=8F=20=EC=9E=90=EC=82=B0=20?= =?UTF-8?q?(Assets)=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=20=20=20=20=20=20?= =?UTF-8?q?=E2=80=A2=20=EA=B7=B9=EB=B9=84=20=EC=B9=B4=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=ED=96=88=EC=9D=84=20=EB=95=8C=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=ED=95=9C=20=EC=9E=90=EC=82=B0=EC=9D=84=20=EC=96=BB?= =?UTF-8?q?=EC=96=B4=20=ED=95=98=EB=8B=A8=20=EC=9D=B8=EB=B2=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=8A=AC=EB=A1=AF=EC=97=90=20=EC=8B=9C=EA=B0=81?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=88=84=EC=A0=81=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EC=B4=88=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EB=B6=99=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=20=20=20=20?= =?UTF-8?q?=20=20=E2=80=A2=20=EC=8A=A4=EC=99=80=EC=9D=B4=ED=94=84=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=EC=9D=98=20=EB=93=9C=EB=9E=98=EA=B7=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=96=91=EB=B0=A9=ED=96=A5(XY)=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=ED=97=88=EC=9A=A9=ED=95=98=EC=97=AC,=20=EA=B7=B9=EB=B9=84?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=EC=9D=98=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20'=EC=9C=84=EB=A1=9C=20=EC=8A=A4=EC=99=80=EC=9D=B4?= =?UTF-8?q?=ED=94=84'=ED=95=98=EC=97=AC=20=EB=B3=B4=EB=A5=98/=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=EA=B0=80=20=20=20=20=20=20=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=96=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20=20=204.=20=EC=97=B0=EB=8C=80=EA=B8=B0=20?= =?UTF-8?q?(Chronicle)=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=20=20=20=20=20=20?= =?UTF-8?q?=E2=80=A2=20=EC=A2=8C=EC=83=81=EB=8B=A8=20=EB=82=B4=EB=9F=AC?= =?UTF-8?q?=ED=8B=B0=EB=B8=8C=20=ED=83=9C=EA=B7=B8=20=EB=B0=B0=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=ED=81=B4=EB=A6=AD=ED=95=98=EB=A9=B4=20=EA=B7=B8?= =?UTF-8?q?=EB=8F=99=EC=95=88=20=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EA=B0=80=20=EA=B2=B0=EC=9E=AC=ED=95=9C=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=99=80=20=EC=84=A0=ED=83=9D(=EC=A2=8C/=EC=9A=B0/?= =?UTF-8?q?=EC=9C=84)=EC=9D=84=20=EA=B8=B0=EB=A1=9D=ED=95=9C=20=20Chronicl?= =?UTF-8?q?ePopup=20=20=20=20=20=20=20=EC=9D=B4=20=EB=85=B8=EC=B6=9C?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84=ED=96=88?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=20=205.=20=EB=A9=80=ED=8B=B0=20?= =?UTF-8?q?=EC=97=94=EB=94=A9=20=EB=B0=8F=20=EB=8F=84=EA=B0=90=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=20=20=20=20=20=20=E2=80=A2=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=95=9C=20=EA=B2=8C=EC=9E=84=20=EC=98=A4=EB=B2=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=84=20=EB=8C=80=EC=B2=B4=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=20EndingScreen=20=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=A0=9C=EC=9E=91=ED=96=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20=20=20=20=20=20=20=E2=80=A2=204=EB=8C=80?= =?UTF-8?q?=20=EB=B6=95=EA=B4=B4=20=EC=A1=B0=EA=B1=B4(=EC=82=AC=EC=9E=84,?= =?UTF-8?q?=20=EC=84=B8=EA=B3=84=20=EB=A9=B8=EB=A7=9D,=20=EB=AC=B8?= =?UTF-8?q?=EB=AA=85=20=EB=B6=95=EA=B4=B4,=20=EC=A0=95=EC=B9=98=EC=A0=81?= =?UTF-8?q?=20=ED=83=84=ED=95=B5)=20=EC=A4=91=20=EC=96=B4=EB=8A=90=20?= =?UTF-8?q?=EA=B2=83=EC=9D=84=20=EB=8B=AC=EC=84=B1=ED=96=88=EB=8A=94?= =?UTF-8?q?=EC=A7=80=20=ED=8C=90=EB=B3=84=ED=95=98=EC=97=AC=20=EC=9B=90?= =?UTF-8?q?=EC=9D=B8=EA=B3=BC=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=ED=95=B4=20=20=20=20=20=20=20=EC=A4=8D=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20=20=20=20=20=20=20=E2=80=A2=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=EC=9D=98=20=20localStorage=20=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=B4=20=EC=88=98=EC=A7=91=EB=90=9C=20?= =?UTF-8?q?=EC=97=94=EB=94=A9=EC=9D=B4=20=EB=8F=84=EA=B0=90=EC=97=90=20?= =?UTF-8?q?=EC=98=81=EA=B5=AC=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/ChroniclePopup.css | 93 +++++++++++++ client/src/components/ChroniclePopup.jsx | 29 ++++ client/src/components/CouncilScreen.css | 150 ++++++++++++++++++++ client/src/components/CouncilScreen.jsx | 168 +++++++++++++++++++++++ client/src/components/EndingScreen.css | 44 ++++++ client/src/components/EndingScreen.jsx | 56 ++++++++ client/src/components/GameScreen.css | 10 +- client/src/components/GameScreen.jsx | 96 +++++++++++-- client/src/components/SwipeCard.css | 7 + client/src/components/SwipeCard.jsx | 24 +++- client/src/data/cards.csv | 65 ++++----- client/src/index.css | 3 + client/update_csv.js | 27 ++++ 13 files changed, 720 insertions(+), 52 deletions(-) create mode 100644 client/src/components/ChroniclePopup.css create mode 100644 client/src/components/ChroniclePopup.jsx create mode 100644 client/src/components/CouncilScreen.css create mode 100644 client/src/components/CouncilScreen.jsx create mode 100644 client/src/components/EndingScreen.css create mode 100644 client/src/components/EndingScreen.jsx create mode 100644 client/update_csv.js diff --git a/client/src/components/ChroniclePopup.css b/client/src/components/ChroniclePopup.css new file mode 100644 index 0000000..98ae3c3 --- /dev/null +++ b/client/src/components/ChroniclePopup.css @@ -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; +} diff --git a/client/src/components/ChroniclePopup.jsx b/client/src/components/ChroniclePopup.jsx new file mode 100644 index 0000000..1398aa6 --- /dev/null +++ b/client/src/components/ChroniclePopup.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import './ChroniclePopup.css'; + +export default function ChroniclePopup({ chronicle, onClose }) { + return ( +
+
e.stopPropagation()}> +
+

국장의 연대기

+ +
+
+ {chronicle.length === 0 ? ( +

아직 기록된 역사가 없습니다.

+ ) : ( +
    + {chronicle.map((entry, idx) => ( +
  • + Turn {entry.turn} +

    {entry.text}

    +
  • + ))} +
+ )} +
+
+
+ ); +} diff --git a/client/src/components/CouncilScreen.css b/client/src/components/CouncilScreen.css new file mode 100644 index 0000000..5c8d43b --- /dev/null +++ b/client/src/components/CouncilScreen.css @@ -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; +} diff --git a/client/src/components/CouncilScreen.jsx b/client/src/components/CouncilScreen.jsx new file mode 100644 index 0000000..32e7582 --- /dev/null +++ b/client/src/components/CouncilScreen.jsx @@ -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 ( +
+
+

[📜 이번 주 상정: {agenda}]

+
+ 위험 수용 계수: +
+
+
+
+ {impeachment &&
[🚨 정치적 탄핵안 상정!]
} +
+ +
+ {members.map(m => ( +
+ 👤 + + {m.vote === '찬성' ? '✅' : m.vote === '반대' ? '❌' : '❓'} + +
+ ))} +
+ + {result ? ( +
+

결과: {result}

+ +
+ ) : ( +
+ + + + + {actionTaken && ( + + )} + {!actionTaken && ( + + )} +
+ )} +
+ ); +} diff --git a/client/src/components/EndingScreen.css b/client/src/components/EndingScreen.css new file mode 100644 index 0000000..4b4b06b --- /dev/null +++ b/client/src/components/EndingScreen.css @@ -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; +} diff --git a/client/src/components/EndingScreen.jsx b/client/src/components/EndingScreen.jsx new file mode 100644 index 0000000..8729e0f --- /dev/null +++ b/client/src/components/EndingScreen.jsx @@ -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 ( +
+

{endingInfo.title}

+
+

{endingInfo.desc}

+
+ 발동 원인: {endingInfo.reason} +
+
+ 획득한 내러티브 태그: {narrativeTags}개 +
+
+ +
+ ); +} diff --git a/client/src/components/GameScreen.css b/client/src/components/GameScreen.css index 9cd06ec..dbc0c0d 100644 --- a/client/src/components/GameScreen.css +++ b/client/src/components/GameScreen.css @@ -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 */ diff --git a/client/src/components/GameScreen.jsx b/client/src/components/GameScreen.jsx index 1bf6af1..d6c4447 100644 --- a/client/src/components/GameScreen.jsx +++ b/client/src/components/GameScreen.jsx @@ -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 ( -
-

시스템 붕괴

-

프로젝트가 파멸을 맞이했습니다. 임무 실패.

- -
+ window.location.reload()} + /> + ); + } + + if (showCouncil) { + return ( + ); } @@ -137,7 +197,7 @@ export function GameScreen() {
{/* Top Bar */}
-
+
setShowChronicle(true)} style={{cursor: 'pointer'}}> {narrativeTags}/8
@@ -149,6 +209,10 @@ export function GameScreen() { 🌀
{renderDots(params.entropy)}
+
= 4 ? 'critical' : ''}`} style={{'--param-color': 'var(--color-panic-critical)'}}> + 😱 +
{renderDots(params.panic)}
+
= 4 ? 'critical' : ''}`} style={{'--param-color': 'var(--color-risk-critical)'}}> ⚠️
{renderDots(params.risk)}
@@ -178,12 +242,20 @@ export function GameScreen() {
{tags.surveillance}
-
-
-
-
+ {[0, 1, 2, 3].map(i => ( +
+ {assets[i] ? {assets[i].icon} : ''} +
+ ))}
+ + {showChronicle && ( + setShowChronicle(false)} + /> + )}
); } diff --git a/client/src/components/SwipeCard.css b/client/src/components/SwipeCard.css index e59ecf6..7b41b55 100644 --- a/client/src/components/SwipeCard.css +++ b/client/src/components/SwipeCard.css @@ -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; diff --git a/client/src/components/SwipeCard.jsx b/client/src/components/SwipeCard.jsx index e561761..970e917 100644 --- a/client/src/components/SwipeCard.jsx +++ b/client/src/components/SwipeCard.jsx @@ -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: , 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 }) {
{card.right?.text} {renderStats(card.right?.stats)} + {card.type === '극비' && ( + + 보류 / 버리기 + + )}
); diff --git a/client/src/data/cards.csv b/client/src/data/cards.csv index 683c97f..3e112d7 100644 --- a/client/src/data/cards.csv +++ b/client/src/data/cards.csv @@ -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 +,,,,,,,,,,,,,,,,,,,,,,,,, \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 5790dd7..a23cccc 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -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; diff --git a/client/update_csv.js b/client/update_csv.js new file mode 100644 index 0000000..74f099e --- /dev/null +++ b/client/update_csv.js @@ -0,0 +1,27 @@ +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; + 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; + } +}); + +const newCsv = Papa.unparse(parsed.data, { + columns: [ + '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' + ] +}); + +fs.writeFileSync('./src/data/cards.csv', newCsv); +console.log('CSV updated');