주간 의회 버그 픽스 및 코어 시스템 고도화 계획 진행 (적용 안된것 많음. 체크 필요)

This commit is contained in:
Kyoung5seo
2026-04-29 23:59:25 +09:00
parent 26f8c6593a
commit 4d3e55a89b
2 changed files with 270 additions and 78 deletions

1
src/data/mysteries.csv Normal file
View File

@@ -0,0 +1 @@
id,title,req_tags,cost_anomaly,reward_asset_id,penalty_entropy
1 id title req_tags cost_anomaly reward_asset_id penalty_entropy

View File

@@ -64,6 +64,8 @@
.clock-hand { position: absolute; top: 50%; left: 50%; width: 45%; height: 2px; background: #fff; transform-origin: left center; transform: rotate(270deg); transition: transform 0.5s; } .clock-hand { position: absolute; top: 50%; left: 50%; width: 45%; height: 2px; background: #fff; transform-origin: left center; transform: rotate(270deg); transition: transform 0.5s; }
.share-bars { width: 100%; display: flex; height: 6px; background: #000; border-radius: 3px; overflow: hidden; margin-top: 5px; } .share-bars { width: 100%; display: flex; height: 6px; background: #000; border-radius: 3px; overflow: hidden; margin-top: 5px; }
.share-segment { height: 100%; transition: width 0.3s; } .share-segment { height: 100%; transition: width 0.3s; }
.share-donut { width: 36px; height: 36px; border-radius: 50%; display: flex; justify-content: center; align-items: center; position: relative; }
.donut-hole { width: 22px; height: 22px; border-radius: 50%; background: #1a1a1a; position: absolute; }
.desk-area { flex: 1; position: relative; display: flex; justify-content: center; align-items: center; perspective: 1000px; background: #2d1b14; touch-action: none; overflow: hidden; } .desk-area { flex: 1; position: relative; display: flex; justify-content: center; align-items: center; perspective: 1000px; background: #2d1b14; touch-action: none; overflow: hidden; }
@@ -195,18 +197,22 @@
<div class="gauge-row"> <div class="gauge-row">
<div class="stat-item"><div class="trust-bar"><div class="trust-fill" id="bar-trust"></div></div><span class="stat-label">신임도 <span id="txt-trust">80%</span></span></div> <div class="stat-item"><div class="trust-bar"><div class="trust-fill" id="bar-trust"></div></div><span class="stat-label">신임도 <span id="txt-trust">80%</span></span></div>
<div class="stat-item"><div class="entropy-clock"><div class="clock-face"></div><div class="clock-hand" id="hand-entropy"></div></div><span class="stat-label">종말시계</span></div> <div class="stat-item"><div class="entropy-clock"><div class="clock-face"></div><div class="clock-hand" id="hand-entropy"></div></div><span class="stat-label">종말시계</span></div>
<div class="stat-item" style="width: 35%;" onclick="openModal('faction-modal')"><div class="share-bars" id="bar-share"></div><span class="stat-label">지분 <i class="fa-solid fa-circle-info text-[8px]"></i></span></div> <div class="stat-item" style="width: 35%;" onclick="openModal('faction-modal')"><div class="share-donut" id="donut-share"><div class="donut-hole"></div></div><span class="stat-label">성향(지분) <i class="fa-solid fa-circle-info text-[8px]"></i></span></div>
</div> </div>
<div id="engine-hud" class="mt-2 flex justify-between items-center text-[10px] bg-gray-800 p-1 px-3 w-full border border-gray-600 shadow-inner rounded" style="margin-top: 8px;"> <div id="engine-hud" class="mt-2 text-[10px] bg-gray-800 p-2 w-full border border-gray-600 shadow-inner rounded" style="margin-top: 8px;">
<div><i class="fa-solid fa-microchip text-yellow-500"></i> 누적 태그: <span id="hud-tags" class="text-white font-bold ml-1">없음</span></div> <div class="flex justify-between items-center mb-1">
<div class="font-bold text-gray-300"><i class="fa-solid fa-microchip text-yellow-500"></i> 누적 빌드업 태그</div>
<div id="hud-synergy-badge" class="text-gray-500 transition-colors duration-500"><i class="fa-solid fa-bolt"></i> 연쇄 없음</div> <div id="hud-synergy-badge" class="text-gray-500 transition-colors duration-500"><i class="fa-solid fa-bolt"></i> 연쇄 없음</div>
</div> </div>
<div id="hud-tags-container" class="flex flex-wrap gap-1 mt-1"></div>
</div>
</div> </div>
<div class="desk-area"> <div class="desk-area">
<div class="doc-card" id="current-card" style="display:none;"> <div class="doc-card" id="current-card" style="display:none;">
<div class="folder-tab" id="c-dept">부서</div> <div class="folder-tab" id="c-dept">부서</div>
<div id="c-conflict" class="absolute top-2 right-2 font-bold text-yellow-500 bg-gray-800 px-3 py-1 rounded shadow-md border border-yellow-700 hidden text-[11px] z-10" style="transform: rotate(2deg);"></div>
<div class="warning-postit" id="c-warning">경고</div> <div class="warning-postit" id="c-warning">경고</div>
<div class="lock-chain-overlay" id="lock-overlay"><i class="fa-solid fa-link chains"></i><div class="lock-msg" id="lock-overlay-msg"><span id="lock-type-text"></span><br><span id="lock-reason" class="text-yellow-300 text-[10px] mt-1 block"></span></div></div> <div class="lock-chain-overlay" id="lock-overlay"><i class="fa-solid fa-link chains"></i><div class="lock-msg" id="lock-overlay-msg"><span id="lock-type-text"></span><br><span id="lock-reason" class="text-yellow-300 text-[10px] mt-1 block"></span></div></div>
<div class="law-warning-popup" id="law-popup"><div class="law-title" id="law-popup-title"></div><div class="law-desc" id="law-popup-desc"></div></div> <div class="law-warning-popup" id="law-popup"><div class="law-title" id="law-popup-title"></div><div class="law-desc" id="law-popup-desc"></div></div>
@@ -232,9 +238,10 @@
<div class="p-row">엔트로피: <span id="p-entropy"></span></div> <div class="p-row">엔트로피: <span id="p-entropy"></span></div>
<div class="p-row mt-1">신임도: <span id="p-trust"></span></div> <div class="p-row mt-1">신임도: <span id="p-trust"></span></div>
<hr class="border-gray-600 my-1"> <hr class="border-gray-600 my-1">
<div class="text-[9px] font-bold text-gray-300">태그 & 우호도 변화:</div> <div class="text-[9px] font-bold text-gray-300">획득 태그 (빌드업):</div>
<div id="p-tag-gain" class="text-[9px] text-yellow-400 mt-1"></div> <div id="p-tag-gain" class="text-[10px] font-bold text-yellow-400 mt-1 mb-2 bg-gray-800 rounded px-1 py-1"></div>
<div id="p-faction-change" class="text-[9px] mt-1"></div> <div class="text-[9px] font-bold text-gray-300">성향 태그 (우호도) 변화:</div>
<div id="p-faction-change" class="text-[9px] mt-1 bg-gray-800 rounded px-1 py-1"></div>
</div> </div>
</div> </div>
<div class="overlay-stamp stamp-approve" id="stamp-approve">승인</div> <div class="overlay-stamp stamp-approve" id="stamp-approve">승인</div>
@@ -283,6 +290,7 @@
<div class="mt-3"><div class="flex justify-between text-[9px] font-bold mb-1"><span class="text-blue-400">A 지지 (<span id="vote-a-val">50</span>%)</span><span class="text-red-400">B 지지 (<span id="vote-b-val">50</span>%)</span></div><div class="vote-bar-container"><div class="majority-line"></div><div class="vote-segment bg-blue-600" id="vote-a" style="width:50%"></div><div class="vote-segment bg-red-600" id="vote-b" style="width:50%"></div></div></div> <div class="mt-3"><div class="flex justify-between text-[9px] font-bold mb-1"><span class="text-blue-400">A 지지 (<span id="vote-a-val">50</span>%)</span><span class="text-red-400">B 지지 (<span id="vote-b-val">50</span>%)</span></div><div class="vote-bar-container"><div class="majority-line"></div><div class="vote-segment bg-blue-600" id="vote-a" style="width:50%"></div><div class="vote-segment bg-red-600" id="vote-b" style="width:50%"></div></div></div>
<div class="grid grid-cols-2 gap-2 mt-3 text-[8px]"><div><div class="text-blue-400 border-b border-blue-900 mb-1">A 찬성 파벌</div><div id="list-support-a" class="text-gray-400 space-y-1"></div></div><div><div class="text-red-400 border-b border-red-900 mb-1">B 찬성 파벌</div><div id="list-support-b" class="text-gray-400 space-y-1"></div></div></div> <div class="grid grid-cols-2 gap-2 mt-3 text-[8px]"><div><div class="text-blue-400 border-b border-blue-900 mb-1">A 찬성 파벌</div><div id="list-support-a" class="text-gray-400 space-y-1"></div></div><div><div class="text-red-400 border-b border-red-900 mb-1">B 찬성 파벌</div><div id="list-support-b" class="text-gray-400 space-y-1"></div></div></div>
<div class="mt-3 pt-3 border-t border-gray-700"><div class="text-[9px] text-yellow-500 mb-1 font-bold">소수 정당 로비 (정치 자본 -10)</div><div class="lobby-target-list" id="lobby-targets"></div></div> <div class="mt-3 pt-3 border-t border-gray-700"><div class="text-[9px] text-yellow-500 mb-1 font-bold">소수 정당 로비 (정치 자본 -10)</div><div class="lobby-target-list" id="lobby-targets"></div></div>
<div class="mt-2 pt-2 border-t border-gray-700"><div class="text-[9px] text-red-500 mb-1 font-bold">블랙리스트 탄압 (무료, 영구적 파국)</div><div class="lobby-target-list" id="blacklist-targets"></div></div>
</div> </div>
</div> </div>
<div class="action-btn" onclick="endCouncil()">결과 확정 및 다음 주 시작</div> <div class="action-btn" onclick="endCouncil()">결과 확정 및 다음 주 시작</div>
@@ -318,7 +326,8 @@
<script> <script>
const SHEET_URLS = { const SHEET_URLS = {
factions: "./data/factions.csv", agendas: "./data/agendas.csv", quests: "./data/quests.csv", factions: "./data/factions.csv", agendas: "./data/agendas.csv", quests: "./data/quests.csv",
bills: "./data/bills.csv", assets: "./data/assets.csv", scenarios: "./data/scenarios.csv" bills: "./data/bills.csv", assets: "./data/assets.csv", scenarios: "./data/scenarios.csv",
mysteries: "./data/mysteries.csv"
}; };
const EFFECT_NAMES = { anomaly: "데이터", political: "정치 자본", trust: "신임도", entropy: "엔트로피" }; const EFFECT_NAMES = { anomaly: "데이터", political: "정치 자본", trust: "신임도", entropy: "엔트로피" };
@@ -333,65 +342,86 @@
}; };
function normalizeKey(str) { return String(str).replace(/^\uFEFF/, '').trim().toLowerCase(); } function normalizeKey(str) { return String(str).replace(/^\uFEFF/, '').trim().toLowerCase(); }
function normalizeValue(val) { return typeof val === 'string' ? val.trim() : val; }
function parseCSV(url) { function parseCSV(url) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!url || url.trim() === "") { resolve(null); return; } if (!url || url.trim() === "") { resolve(null); return; }
Papa.parse(url, { Papa.parse(url, {
header: true, skipEmptyLines: true, download: true, header: true, skipEmptyLines: true, download: true,
complete: function(results) { resolve(results.data.map(row => { let r={}; for(let k in row) r[normalizeKey(k)]=row[k]; return r; })); }, complete: function(results) {
resolve(results.data.map(row => {
let r={};
for(let k in row) r[normalizeKey(k)] = normalizeValue(row[k]);
return r;
}));
},
error: function() { resolve(null); } error: function() { resolve(null); }
}); });
}); });
} }
function loadFallbackData() {
if (DB_FACTIONS.length === 0) DB_FACTIONS = [
{ id: 'iron', name: '철의 수호당', color: '#ef4444', share: 30, rival: 'libra', like_tag: '군사', hate_tag: '오컬트', desc: "괴수 즉결 처형 및 군사력 증강" },
{ id: 'libra', name: '지식 보존당', color: '#3b82f6', share: 20, rival: 'iron', like_tag: '과학', hate_tag: '군사', desc: "생포 및 연구를 통한 이해" },
{ id: 'pure', name: '순수 인간당', color: '#10b981', share: 15, rival: 'gene', like_tag: '순수', hate_tag: '변이', desc: "인간 중심주의" },
{ id: 'trade', name: '차원 무역당', color: '#f59e0b', share: 10, rival: 'covenant', like_tag: '자본', hate_tag: '숭배', desc: "괴수 무역을 통한 자본 축적" }
];
if (DB_AGENDAS.length === 0) DB_AGENDAS = [{ id: "AG_01", title: "특별 경계 주간", desc: "태그 획득 가속" }];
if (DB_QUESTS.length === 0) DB_QUESTS = [{ id: "Q_01", title: "승인 2회", target: { val: 2 }, reward_txt: "정치자본 +20", penalty_txt: "신임도 -10" }];
if (DB_BILLS.length === 0) DB_BILLS = [
{ id: "B_01", name: "종합 군사 지원 특별법", desc: "[군사] 태그 혜택 및 미스터리 소모 할인", tag: "none", target_tag: "군사", type: "buff_tag", buff_tag: "군사", flavor: "힘이 정의다.", is_buff: true },
{ id: "B_02", name: "기초 과학 집중 육성", desc: "[과학] 태그 혜택 증대", tag: "none", target_tag: "과학", type: "buff_tag", buff_tag: "과학", flavor: "연구가 미래다.", is_buff: true },
{ id: "B_03", name: "자본 감축 조약", desc: "모든 [자본] 자산 무효화 및 페널티", tag: "none", target_tag: "자본", type: "nullify_tag", nullify_tag: "자본", flavor: "부패를 척결하겠습니다.", is_buff: false },
{ id: "B_04", name: "사살 및 군사 억제법", desc: "모든 [군사] 무효화 및 작전 승인 금지", tag: "kill", target_tag: "군사", type: "lock_approve", nullify_tag: "군사", flavor: "폭력은 답이 아닙니다.", is_buff: false }
];
if (DB_ASSETS.length === 0) DB_ASSETS = [
{ id: "AST_001", name: "유령 커피머신 (Relic)", desc: "커피 맛은 천상, 밤마다 악몽.", passive_effect: { anomaly: 5 }, holding_risk: { entropy: 1 }, tags: ["군사"], combo_req: 2, combo_effect_key: "trust", combo_effect_val: 2 },
{ id: "AST_002", name: "비밀 해커팀 (Agent)", desc: "정보망을 통한 횡령.", passive_effect: { political: 5 }, tags: ["과학", "자본"], combo_req: 1, combo_effect_key: "anomaly", combo_effect_val: 10 }
];
if (DB_SCENARIOS.length === 0) DB_SCENARIOS = [
{ id: 101, act: 1, dept: "행정 본부", color: "#d97706", title: "탕비실 믹스커피 횡령", body: "요원들이 비품을 횡령했습니다.", faction: "지식 보존당", tags: ["과학"], yes_a: 10, no_p: 5, yes_e: 0, no_e: +1, flavor: "맥심 골드를 지켜야 합니다." },
{ id: 102, act: 1, dept: "작전 본부", color: "#c92a2a", title: "하급 슬라임 민원", body: "하수구 슬라임을 소각하시겠습니까?", faction: "철의 수호당", tags: ["군사"], yes_a: 15, no_p: 10, yes_e: -5, no_e: +5, flavor: "소금 뿌리면 안 될까요?" }
];
if (DB_MYSTERIES.length === 0) DB_MYSTERIES = [
{ id: "M_01", title: "지하실의 악마", req_tags: { "군사": 2 }, cost_anomaly: 15, reward_asset_id: "AST_001", penalty_entropy: 1 },
{ id: "M_02", title: "사설 정보망 해킹", req_tags: { "과학": 1 }, cost_anomaly: 5, reward_asset_id: "AST_002", penalty_entropy: 0 }
];
}
async function initDB() { async function initDB() {
document.getElementById('loading-screen').style.display = 'flex'; document.getElementById('loading-screen').style.display = 'flex';
DB_FACTIONS = []; DB_AGENDAS = []; DB_QUESTS = []; DB_BILLS = []; DB_ASSETS = []; DB_SCENARIOS = []; DB_MYSTERIES = [];
try { try {
const [factions, agendas, quests, bills, assets, scenarios] = await Promise.all([ const [factions, agendas, quests, bills, assets, scenarios, mysteries] = await Promise.all([
parseCSV(SHEET_URLS.factions), parseCSV(SHEET_URLS.agendas), parseCSV(SHEET_URLS.quests), parseCSV(SHEET_URLS.factions), parseCSV(SHEET_URLS.agendas), parseCSV(SHEET_URLS.quests),
parseCSV(SHEET_URLS.bills), parseCSV(SHEET_URLS.assets), parseCSV(SHEET_URLS.scenarios) parseCSV(SHEET_URLS.bills), parseCSV(SHEET_URLS.assets), parseCSV(SHEET_URLS.scenarios),
parseCSV(SHEET_URLS.mysteries)
]); ]);
// parse and normalize basically omitted for brevity in robust fallback
throw new Error("Local fallback enforced for new Logic transition"); if (factions && factions.length > 0) {
} catch(e) { DB_FACTIONS = factions.map(f => ({
loadFallbackData(); ...f,
setTimeout(() => { document.getElementById('loading-screen').style.display = 'none'; startGame(); }, 500); share: parseInt(f.share)||10,
like_tag: f.love_tag || f.like_tag || '',
hate_tag: f.hate_tag || ''
}));
} }
if (agendas && agendas.length > 0) DB_AGENDAS = agendas;
if (quests && quests.length > 0) DB_QUESTS = quests;
if (bills && bills.length > 0) {
DB_BILLS = bills.map(b => {
let is_buff = b.type === 'lock_reject' || b.type === 'buff_tag';
return {
...b,
is_buff: is_buff,
target_tag: b.tag,
buff_tag: is_buff ? b.tag : null,
nullify_tag: !is_buff ? b.tag : null
};
});
}
if (assets && assets.length > 0) {
DB_ASSETS = assets.map(a => ({
...a,
tags: a.tags ? a.tags.split(',').map(t=>t.trim()) : [],
passive_effect: { anomaly: parseInt(a.passive_eff_val)||0 }, // 간단한 호환성 매핑
holding_risk: { entropy: parseInt(a.risk_eff_val)||0 }
}));
}
if (scenarios && scenarios.length > 0) {
DB_SCENARIOS = scenarios.map(s => ({
...s,
act: parseInt(s.act)||1,
tags: s.tags ? s.tags.split(',').map(t=>t.trim().replace(/^"|"$/g, '')) : [],
yes_a: parseInt(s.cost)||0, // cost를 데이터(anomaly) 획득/소모로 매핑
no_p: parseInt(s.conflict)||0, // conflict를 정치 자본 획득/소모로 매핑
yes_e: parseInt(s.yes_e)||0,
no_e: parseInt(s.no_e)||0,
}));
}
if (mysteries && mysteries.length > 0) {
DB_MYSTERIES = mysteries.map(m => {
let req_tags = {};
if(m.req_tags) m.req_tags.split(',').forEach(rt => { let parts = rt.split(':'); if(parts.length===2) req_tags[parts[0].trim()] = parseInt(parts[1]); });
return { ...m, req_tags: req_tags, cost_anomaly: parseInt(m.cost_anomaly)||0, penalty_entropy: parseInt(m.penalty_entropy)||0 };
});
}
} catch(e) {
console.error("CSV 파싱 에러:", e);
}
setTimeout(() => { document.getElementById('loading-screen').style.display = 'none'; startGame(); }, 500);
} }
function startGame() { function startGame() {
@@ -407,15 +437,58 @@
updateHUD(); nextDay(); updateHUD(); nextDay();
} }
function getAffinity(f) {
if (f.is_blacklisted) return { level: 5, name: "🔥 파국", color: "#ef4444" };
if (f.share >= 40) return { level: 1, name: "👑 핵심 동맹", color: "#3b82f6" };
if (f.share >= 20) return { level: 2, name: "🤝 우호", color: "#10b981" };
if (f.share >= 10) return { level: 3, name: "⚖️ 중립", color: "#9ca3af" };
return { level: 4, name: "⚔️ 적대", color: "#f59e0b" };
}
function nextDay() { function nextDay() {
if (state.day > 5) { startCouncil(); return; } if (state.day > 5) { startCouncil(); return; }
if (state.entropy >= 100) { alert("종말 시계 100% 도달. 통제에 실패했습니다."); initDB(); return; }
applyAssetPassives(); applyAssetPassives();
state.activeMysteries.forEach(m => { if(m.penalty_entropy) state.entropy += m.penalty_entropy; }); state.activeMysteries.forEach(m => { if(m.penalty_entropy) state.entropy += m.penalty_entropy; });
let panicLevel = 1; let panicMsg = "";
if (state.entropy >= 81) panicLevel = 5;
else if (state.entropy >= 61) panicLevel = 4;
else if (state.entropy >= 41) panicLevel = 3;
else if (state.entropy >= 21) panicLevel = 2;
if (panicLevel === 2) { addStat('trust', -1); panicMsg = "[동요] 시민 불안 (신임도 -1)"; }
else if (panicLevel >= 3) { addStat('political', -1); addStat('trust', -2); panicMsg = "[혼란] 시스템 마비 (자본/신임도 하락)"; }
let terrorChance = panicLevel >= 4 ? 0.6 : 0.3;
let sabotageMsg = [];
if (panicMsg) sabotageMsg.push(panicMsg);
DB_FACTIONS.forEach(f => {
if(getAffinity(f).level === 5 && Math.random() < terrorChance) {
state.entropy += 5; state.trust -= 5;
sabotageMsg.push(`[파국] ${f.name} 테러!`);
}
});
if (sabotageMsg.length > 0) {
const toast = document.getElementById('passive-toast');
toast.innerText = sabotageMsg.join(' / ');
toast.className = 'passive-toast bg-red-600'; void toast.offsetWidth; toast.classList.add('active');
}
if (panicLevel === 5) {
state.currentCard = {
id: 999, dept: "재난 관리", color: "#991b1b",
title: "현실 붕괴 임박", body: "사회 시스템이 통제 불능 상태입니다. 막대한 자원을 희생해 시간을 벌겠습니까?",
faction: "전체 파벌", tags: [], yes_a: 40, no_p: 20, yes_e: -10, no_e: 15, flavor: "신은 우리를 버렸습니다."
};
} else {
let pool = DB_SCENARIOS.filter(s => s.act === state.week); let pool = DB_SCENARIOS.filter(s => s.act === state.week);
if(pool.length === 0) pool = DB_SCENARIOS; if(pool.length === 0) pool = DB_SCENARIOS;
state.currentCard = pool[Math.floor(Math.random() * pool.length)]; state.currentCard = pool[Math.floor(Math.random() * pool.length)];
}
renderCard(state.currentCard); renderCard(state.currentCard);
const cardEl = document.getElementById('current-card'); const cardEl = document.getElementById('current-card');
@@ -527,19 +600,87 @@
document.getElementById('bar-trust').style.width = `${state.trust}%`; document.getElementById('txt-trust').innerText = `${state.trust}%`; document.getElementById('bar-trust').style.width = `${state.trust}%`; document.getElementById('txt-trust').innerText = `${state.trust}%`;
document.getElementById('hand-entropy').style.transform = `translateX(-50%) rotate(${(state.entropy / 100) * 360}deg)`; document.getElementById('hand-entropy').style.transform = `translateX(-50%) rotate(${(state.entropy / 100) * 360}deg)`;
const sb = document.getElementById('bar-share'); sb.innerHTML = "";
let sortedFactions = [...DB_FACTIONS].sort((a,b)=>b.share - a.share); let sortedFactions = [...DB_FACTIONS].sort((a,b)=>b.share - a.share);
sortedFactions.forEach(f => { if(f.share > 0) sb.innerHTML += `<div class="share-segment" style="width:${f.share}%; background:${f.color};"></div>`; });
// Donut Chart
const donut = document.getElementById('donut-share');
if(donut) {
let conicStr = []; let currentDeg = 0;
sortedFactions.forEach(f => {
if(f.share > 0) {
let deg = (f.share / 100) * 360;
conicStr.push(`${f.color} ${currentDeg}deg ${currentDeg + deg}deg`);
currentDeg += deg;
}
});
donut.style.background = `conic-gradient(${conicStr.join(', ')})`;
}
document.getElementById('asset-count').innerText = state.assets.length; document.getElementById('asset-count').innerText = state.assets.length;
let tagsArr = []; const tagsContainer = document.getElementById('hud-tags-container');
for(let t in state.builtTags) tagsArr.push(`${t}(${state.builtTags[t]})`); if(tagsContainer) {
document.getElementById('hud-tags').innerText = tagsArr.length > 0 ? tagsArr.join(' / ') : '없음'; tagsContainer.innerHTML = "";
let tagCount = 0;
for(let t in state.builtTags) {
if(state.builtTags[t] > 0) {
tagsContainer.innerHTML += `<div class="bg-gray-700 text-yellow-300 px-2 py-1 rounded text-[9px] font-bold shadow-sm">[${t}] ${state.builtTags[t]}</div>`;
tagCount++;
}
}
if(tagCount === 0) tagsContainer.innerHTML = "<span class='text-gray-500 italic'>없음</span>";
}
let hasActiveSynergy = state.assets.some(a => a.combo_req > 0 && a.tags && a.tags.some(t => state.builtTags[t] >= a.combo_req)); let hasActiveSynergy = state.assets.some(a => a.combo_req > 0 && a.tags && a.tags.some(t => state.builtTags[t] >= a.combo_req));
document.getElementById('hud-synergy-badge').className = hasActiveSynergy ? "text-yellow-400 font-bold animate-pulse" : "text-gray-500"; document.getElementById('hud-synergy-badge').className = hasActiveSynergy ? "text-yellow-400 font-bold animate-pulse" : "text-gray-500";
document.getElementById('hud-synergy-badge').innerHTML = hasActiveSynergy ? `<i class="fa-solid fa-bolt"></i> 시너지 가동` : `<i class="fa-solid fa-bolt"></i> 연쇄 없음`; document.getElementById('hud-synergy-badge').innerHTML = hasActiveSynergy ? `<i class="fa-solid fa-bolt"></i> 시너지 가동` : `<i class="fa-solid fa-bolt"></i> 연쇄 없음`;
if (state.currentAgenda) {
const agTitle = document.getElementById('agenda-title'); if(agTitle) agTitle.innerText = state.currentAgenda.title;
}
}
function renderCard(data) {
const card = document.getElementById('current-card');
card.style.display = 'flex'; card.style.borderColor = "#cbb"; card.style.transform = "translate(0,0)";
document.getElementById('c-dept').innerText = data.dept || '부서 불명';
document.getElementById('c-dept').style.backgroundColor = data.color || '#333';
document.getElementById('c-title').innerText = data.title || '제목 없음';
document.getElementById('c-body').innerText = data.body || '내용 없음';
document.getElementById('c-flavor').innerText = `"${data.flavor || ''}"`;
document.getElementById('c-faction').innerText = data.faction || '무소속';
let tHtml = "";
if(data.tags) data.tags.forEach(t => tHtml += `<span class="tag-badge">[${t}]</span>`);
const cTags = document.getElementById('c-tags');
if(cTags) cTags.innerHTML = tHtml;
let conflictElem = document.getElementById('c-conflict');
if(conflictElem) {
if (data.no_p > 0) {
conflictElem.innerHTML = `거절 시 👑 +${data.no_p}`;
conflictElem.classList.remove('hidden');
} else {
conflictElem.classList.add('hidden');
}
}
let lockType = "none";
if(data.tags) {
state.activeLaws.forEach(bill => {
if(data.tags.includes(bill.tag)) { if(bill.type === "lock_approve") lockType = "approve"; if(bill.type === "lock_reject") lockType = "reject"; }
});
}
state.lockState = lockType;
const overlay = document.getElementById('lock-overlay');
if (lockType === 'approve') { overlay.classList.add('active'); document.getElementById('lock-type-text').innerText = "승인 금지"; }
else if (lockType === 'reject') { overlay.classList.add('active'); document.getElementById('lock-type-text').innerText = "거절 금지"; }
else overlay.classList.remove('active');
document.getElementById('stamp-approve').style.opacity = 0; document.getElementById('stamp-reject').style.opacity = 0;
document.getElementById('predict-overlay').style.display = 'none';
document.getElementById('law-popup').classList.remove('visible');
} }
const card = document.getElementById('current-card'); const card = document.getElementById('current-card');
@@ -581,27 +722,30 @@
let tGainHtml = ""; let fGainHtml = ""; let tGainHtml = ""; let fGainHtml = "";
if(isApprove) { if(isApprove) {
document.getElementById('p-header').innerText = "승인 (APPROVE)"; document.getElementById('p-header').className = "font-bold border-b border-gray-600 mb-2 pb-1 text-center text-red-400"; document.getElementById('p-header').innerText = "승인 (APPROVE)"; document.getElementById('p-header').className = "font-bold border-b border-gray-600 mb-2 pb-1 text-center text-red-400";
document.getElementById('p-desc').innerText = "부서 전략 수행"; document.getElementById('p-desc').innerText = "엔진 빌드업 (태그 확보)";
document.getElementById('p-anomaly').innerHTML = data.yes_a ? `+${data.yes_a}` : "0"; document.getElementById('p-anomaly').innerHTML = data.yes_a ? `<span class="${data.yes_a>0?'p-down':'p-up'}">${data.yes_a>0?'-':'+'}${Math.abs(data.yes_a)}</span>` : "0";
document.getElementById('p-political').innerHTML = "-"; document.getElementById('p-political').innerHTML = "-";
document.getElementById('p-entropy').innerHTML = data.yes_e ? `<span class="p-down">${data.yes_e > 0?'+':''}${data.yes_e}</span>` : "0"; document.getElementById('p-entropy').innerHTML = data.yes_e ? `<span class="p-down">${data.yes_e > 0?'+':''}${data.yes_e}</span>` : "0";
document.getElementById('p-trust').innerHTML = "-"; document.getElementById('p-trust').innerHTML = "-";
data.tags.forEach(t => tGainHtml += `[${t}] +1 `); data.tags.forEach(t => tGainHtml += `[${t}] +1 `);
data.tags.forEach(t => { fGainHtml = "변동 없음 (침묵)";
DB_FACTIONS.forEach(f => {
if(f.like_tag === t) fGainHtml += ` <span style="color:${f.color}">${f.name} ▲</span>`;
if(f.hate_tag === t) fGainHtml += ` <span style="color:${f.color}">${f.name} ▼</span>`;
});
});
} else { } else {
document.getElementById('p-header').innerText = "거절 (REJECT)"; document.getElementById('p-header').className = "font-bold border-b border-gray-600 mb-2 pb-1 text-center text-blue-400"; document.getElementById('p-header').innerText = "거절 (REJECT)"; document.getElementById('p-header').className = "font-bold border-b border-gray-600 mb-2 pb-1 text-center text-blue-400";
document.getElementById('p-desc').innerText = "관료제 통제 (기각)"; document.getElementById('p-desc').innerText = "관료제 통제 (기각 및 정치공작)";
document.getElementById('p-anomaly').innerHTML = "-"; document.getElementById('p-anomaly').innerHTML = "-";
document.getElementById('p-political').innerHTML = data.no_p ? `+${data.no_p}` : "0"; document.getElementById('p-political').innerHTML = data.no_p ? `+${data.no_p}` : "0";
document.getElementById('p-entropy').innerHTML = data.no_e ? `<span class="p-up">${data.no_e > 0?'+':''}${data.no_e}</span>` : "0"; document.getElementById('p-entropy').innerHTML = data.no_e ? `<span class="p-up">${data.no_e > 0?'+':''}${data.no_e}</span>` : "0";
document.getElementById('p-trust').innerHTML = `<span class="p-up">+2</span>`; document.getElementById('p-trust').innerHTML = `<span class="p-up">+2</span>`;
fGainHtml = "사태 방관으로 인한 정치 국면 유지";
tGainHtml = "<span class='text-gray-500'>획득 불가</span>";
data.tags.forEach(t => {
DB_FACTIONS.forEach(f => {
if(f.like_tag === t) fGainHtml += ` <span style="color:${f.color}">${f.name} ▼▼</span>`;
if(f.hate_tag === t) fGainHtml += ` <span style="color:${f.color}">${f.name} ▲▲</span>`;
});
});
if(fGainHtml === "") fGainHtml = "영향력 변동 없음";
} }
document.getElementById('p-tag-gain').innerHTML = tGainHtml; document.getElementById('p-tag-gain').innerHTML = tGainHtml;
document.getElementById('p-faction-change').innerHTML = fGainHtml; document.getElementById('p-faction-change').innerHTML = fGainHtml;
@@ -611,17 +755,24 @@
const data = state.currentCard; let animateDir = type === 'approve' ? 500 : -500; const data = state.currentCard; let animateDir = type === 'approve' ? 500 : -500;
if (type === 'approve') { if (type === 'approve') {
addStat('anomaly', data.yes_a || 0); addStat('entropy', data.yes_e || 0); addStat('anomaly', -(data.yes_a || 0)); addStat('entropy', data.yes_e || 0);
if(data.tags) { if(data.tags) {
data.tags.forEach(t => { data.tags.forEach(t => {
state.builtTags[t] = (state.builtTags[t]||0) + 1; state.builtTags[t] = (state.builtTags[t]||0) + 1;
DB_FACTIONS.forEach(f=>{ if(f.like_tag===t) f.share+=2; if(f.hate_tag===t) f.share=Math.max(0, f.share-1); });
}); });
} }
if (state.currentQuest) state.questProgress++; if (state.currentQuest) state.questProgress++;
enqueueEvent('on_approve', null); enqueueEvent('on_approve', null);
} else { } else {
addStat('political', data.no_p || 0); addStat('entropy', data.no_e || 0); addStat('trust', 2); addStat('political', data.no_p || 0); addStat('entropy', data.no_e || 0); addStat('trust', 2);
if(data.tags) {
data.tags.forEach(t => {
DB_FACTIONS.forEach(f=>{
if(f.like_tag===t) f.share=Math.max(0, f.share-3);
if(f.hate_tag===t) f.share+=2;
});
});
}
enqueueEvent('on_reject', null); enqueueEvent('on_reject', null);
} }
@@ -658,11 +809,11 @@
<div class="flex justify-between border-b border-gray-600 pb-1 mb-2"><span class="font-bold text-blue-300 text-sm">${m.title}</span><span class="text-xs text-red-400">매턴 엔트로피 +${m.penalty_entropy || 0}</span></div> <div class="flex justify-between border-b border-gray-600 pb-1 mb-2"><span class="font-bold text-blue-300 text-sm">${m.title}</span><span class="text-xs text-red-400">매턴 엔트로피 +${m.penalty_entropy || 0}</span></div>
<div class="text-[10px] text-gray-300 flex justify-between items-center"> <div class="text-[10px] text-gray-300 flex justify-between items-center">
<div> <div>
<div class="mb-1">요구 태그: ${reqHtml}</div> <div class="mb-1 text-yellow-300 font-bold">소모 태그: ${reqHtml}</div>
<div>데이터 비용: 🧬 <span class="${state.anomaly >= finalCost ? 'text-blue-300' : 'text-red-400'}">${finalCost}</span> ${discount>0 ? `<span class="text-green-400 ml-1">(법안 할인 -${discount})</span>`:''}</div> <div>데이터 비용: 🧬 <span class="${state.anomaly >= finalCost ? 'text-blue-300' : 'text-red-400'}">${finalCost}</span> ${discount>0 ? `<span class="text-green-400 ml-1">(법안 할인 -${discount})</span>`:''}</div>
<div class="mt-1 text-green-300 font-bold">보상: ${ast?ast.name:'엔진 자산'}</div> <div class="mt-1 text-green-300 font-bold">보상: ${ast?ast.name:'엔진 자산'}</div>
</div> </div>
<button class="${canSolve?'bg-blue-600 hover:bg-blue-500':'bg-gray-700 opacity-50 cursor-not-allowed'} px-3 py-2 rounded text-white font-bold" onclick="${canSolve?`solveMystery(${idx})`:'return false;'}">비밀 해독</button> <button class="${canSolve?'bg-blue-600 hover:bg-blue-500':'bg-gray-700 opacity-50 cursor-not-allowed'} px-3 py-2 rounded text-white font-bold flex flex-col items-center" onclick="${canSolve?`solveMystery(${idx})`:'return false;'}"><span>해독</span><span class="text-[8px] font-normal">(태그 소모)</span></button>
</div> </div>
</div>`; </div>`;
}); });
@@ -696,8 +847,13 @@
document.getElementById('council-phase-1').classList.remove('active'); document.getElementById('council-phase-2').classList.add('active'); document.getElementById('council-phase-1').classList.remove('active'); document.getElementById('council-phase-2').classList.add('active');
let mainTagInfo = getMainTag(); let targetTag = mainTagInfo.tag; let mainTagInfo = getMainTag(); let targetTag = mainTagInfo.tag;
let sortedFactions = [...DB_FACTIONS].sort((a,b)=>b.share - a.share);
const top = sortedFactions[0]; const hostile = sortedFactions[sortedFactions.length-1]; // Affinity 적용하여 동맹(1,2)과 적대(4,5) 판별
let factionsWithAff = DB_FACTIONS.map(f => ({ ...f, aff: getAffinity(f) }));
let sortedFactions = factionsWithAff.sort((a,b) => a.aff.level - b.aff.level);
const top = sortedFactions[0]; // 가장 우호적인 파벌
const hostile = sortedFactions[sortedFactions.length-1]; // 가장 적대적인 파벌
let buffBills = DB_BILLS.filter(b => b.is_buff); let buffBills = DB_BILLS.filter(b => b.is_buff);
let debuffBills = DB_BILLS.filter(b => !b.is_buff); let debuffBills = DB_BILLS.filter(b => !b.is_buff);
@@ -714,16 +870,26 @@
document.getElementById('bill-a-name').innerText = b1.name; document.getElementById('bill-a-flavor').innerText = `"${b1.flavor}"`; document.getElementById('bill-a-name').innerText = b1.name; document.getElementById('bill-a-flavor').innerText = `"${b1.flavor}"`;
document.getElementById('bill-b-name').innerText = b2.name; document.getElementById('bill-b-flavor').innerText = `"${b2.flavor}"`; document.getElementById('bill-b-name').innerText = b2.name; document.getElementById('bill-b-flavor').innerText = `"${b2.flavor}"`;
let supportersA = [], supportersB = [], swingers = []; let votesA = 0; let supportersA = [], supportersB = [], swingers = [], enemies = []; let votesA = 0;
sortedFactions.forEach(f => {
if(f.id === top.id || (f.like_tag === targetTag)) { supportersA.push(f.name); votesA += f.share; } factionsWithAff.forEach(f => {
else if(f.id === hostile.id || (f.hate_tag === targetTag)) { supportersB.push(f.name); } if(f.aff.level <= 2 || (f.like_tag === targetTag)) { supportersA.push(f.name); votesA += f.share; }
else if(f.aff.level >= 4 || (f.hate_tag === targetTag)) { supportersB.push(f.name); enemies.push(f); }
else { swingers.push(f); } else { swingers.push(f); }
}); });
state.council.votesA = votesA; state.council.supportersA = supportersA; state.council.supportersB = supportersB; updateVoteUI();
state.council.votesA = Math.max(0, votesA);
state.council.supportersA = supportersA;
state.council.supportersB = supportersB;
updateVoteUI();
const lobbyList = document.getElementById('lobby-targets'); lobbyList.innerHTML = ""; const lobbyList = document.getElementById('lobby-targets'); lobbyList.innerHTML = "";
swingers.forEach(f => { lobbyList.innerHTML += `<div class="lobby-tag" onclick="lobbyFaction('${f.id}', event)">${f.name} (${f.share}%)</div>`; }); swingers.forEach(f => { lobbyList.innerHTML += `<div class="lobby-tag" onclick="lobbyFaction('${f.id}', event)">${f.name} (${f.share}%)</div>`; });
const blacklistList = document.getElementById('blacklist-targets'); blacklistList.innerHTML = "";
enemies.forEach(f => {
if(!f.is_blacklisted) blacklistList.innerHTML += `<div class="lobby-tag" style="border-color:#ef4444; color:#ef4444;" onclick="blacklistFaction('${f.id}', event)">${f.name}</div>`;
});
} }
function lobbyFaction(fid, e) { function lobbyFaction(fid, e) {
@@ -734,10 +900,22 @@
updateVoteUI(); updateHUD(); updateVoteUI(); updateHUD();
} }
function blacklistFaction(fid, e) {
const fac = DB_FACTIONS.find(f => f.id === fid);
fac.is_blacklisted = true;
state.council.supportersB = state.council.supportersB.filter(name => name !== fac.name);
const tgt = e.currentTarget; tgt.style.background = "#ef4444"; tgt.style.color = "#fff"; tgt.innerText = "탄압됨"; tgt.onclick = null;
alert(`${fac.name}을(를) 영구적 파국 상태로 몰아넣었습니다. 다음 턴부터 사보타주 테러가 발생할 수 있습니다.`);
updateVoteUI(); updateHUD();
}
function updateVoteUI() { function updateVoteUI() {
const vA = Math.min(100, state.council.votesA); const vA = Math.min(100, state.council.votesA);
document.getElementById('vote-a').style.width = `${vA}%`; document.getElementById('vote-b').style.width = `${100-vA}%`; document.getElementById('vote-a').style.width = `${vA}%`; document.getElementById('vote-b').style.width = `${100-vA}%`;
document.getElementById('vote-a-val').innerText = vA; document.getElementById('vote-b-val').innerText = 100-vA; document.getElementById('vote-a-val').innerText = vA; document.getElementById('vote-b-val').innerText = 100-vA;
document.getElementById('list-support-a').innerText = state.council.supportersA.length > 0 ? state.council.supportersA.join(', ') : '없음';
document.getElementById('list-support-b').innerText = state.council.supportersB.length > 0 ? state.council.supportersB.join(', ') : '없음';
} }
function endCouncil() { function endCouncil() {
@@ -755,6 +933,19 @@
document.getElementById('asset-list').innerHTML = state.assets.map(a => `<div class="list-item"><span class="font-bold flex items-center">${a.name} <span class="bg-gray-700 text-gray-300 px-1 rounded ml-1 text-[8px]">${a.tags?a.tags.join(','):''}</span></span><span class="text-[9px] text-green-400 ml-auto">[작동 중]</span></div>`).join(''); document.getElementById('asset-list').innerHTML = state.assets.map(a => `<div class="list-item"><span class="font-bold flex items-center">${a.name} <span class="bg-gray-700 text-gray-300 px-1 rounded ml-1 text-[8px]">${a.tags?a.tags.join(','):''}</span></span><span class="text-[9px] text-green-400 ml-auto">[작동 중]</span></div>`).join('');
if(state.assets.length===0) document.getElementById('asset-list').innerHTML = "<div class='text-center text-xs text-gray-500'>없음</div>"; if(state.assets.length===0) document.getElementById('asset-list').innerHTML = "<div class='text-center text-xs text-gray-500'>없음</div>";
} }
if(id==='faction-modal') {
let facHtml = "";
let sortedFactions = [...DB_FACTIONS].sort((a,b)=>b.share - a.share);
sortedFactions.forEach(f => {
let aff = getAffinity(f);
facHtml += `<div class="bg-gray-800 p-3 rounded mb-2 border-l-4" style="border-color:${f.color}">
<div class="flex justify-between items-center mb-1"><span class="font-bold" style="color:${f.color}">${f.name}</span><span class="text-xs font-bold" style="color:${aff.color}">${aff.name} (지분 ${f.share}%)</span></div>
<div class="text-[10px] text-gray-400 mb-1">${f.desc}</div>
<div class="text-[9px] text-gray-500">선호: <span class="text-green-400">[${f.like_tag||'없음'}]</span> / 혐오: <span class="text-red-400">[${f.hate_tag||'없음'}]</span></div>
</div>`;
});
document.getElementById('faction-list-detail').innerHTML = facHtml;
}
} }
window.closeModal = (id) => document.getElementById(id).classList.remove('active'); window.closeModal = (id) => document.getElementById(id).classList.remove('active');