Spaces:
Sleeping
Sleeping
이야기 주제 제공 기능 완성
Browse files
npc_social_network/routes/npc_route.py
CHANGED
@@ -136,4 +136,26 @@ def force_relationship():
|
|
136 |
npc2.relationships.set_relationship(npc1.name, relationship_type)
|
137 |
return jsonify({"success": True})
|
138 |
|
139 |
-
return jsonify({"success": False, "error": "NPC not found"}), 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
npc2.relationships.set_relationship(npc1.name, relationship_type)
|
137 |
return jsonify({"success": True})
|
138 |
|
139 |
+
return jsonify({"success": False, "error": "NPC not found"}), 404
|
140 |
+
|
141 |
+
@npc_bp.route("/api/orchestrate_conversation", methods=['Post'])
|
142 |
+
def orchestrate_conversation():
|
143 |
+
"""플레이어가 지정한 두 NPC와 상황으로 대화를 시작시키는 API"""
|
144 |
+
data = request.json
|
145 |
+
npc1_name = data.get("npc1_name")
|
146 |
+
npc2_name = data.get("npc2_name")
|
147 |
+
situation = data.get("situation") # 대화 핵심 'topic'
|
148 |
+
|
149 |
+
if not all([npc1_name, npc2_name, situation]) or npc1_name == npc2_name:
|
150 |
+
return jsonify({"success": False, "error": "Invalid data"}), 400
|
151 |
+
|
152 |
+
# 락을 걸지 않고 매니저에 바로 요청
|
153 |
+
npc1 = simulation_core.npc_manager.get_npc_by_korean_name(npc1_name)
|
154 |
+
npc2 = simulation_core.npc_manager.get_npc_by_korean_name(npc2_name)
|
155 |
+
|
156 |
+
if npc1 and npc2:
|
157 |
+
# 기존의 대화 시작 함수 재활용
|
158 |
+
simulation_core.conversation_manager.start_conversation(npc1, npc2, topic=situation)
|
159 |
+
return jsonify({"success": True})
|
160 |
+
|
161 |
+
return jsonify({"success": False, "error": "NPC not found"}), 404
|
npc_social_network/templates/dashboard.html
CHANGED
@@ -14,10 +14,10 @@
|
|
14 |
#network { width: 100%; flex: 1; border-bottom: 1px solid var(--border-color); }
|
15 |
|
16 |
/* 전체 컨트롤 패널 (가장 바깥쪽) */
|
17 |
-
#controls { padding:
|
18 |
|
19 |
/* 메인 컨트롤 버튼 */
|
20 |
-
#main-controls { display: flex;
|
21 |
#main-controls button { padding: 8px 15px; border-radius: 6px; border: none; cursor: pointer; color: rgb(0, 0, 0); font-size: 14px; font-weight: 500; transition: background-color 0.2s;}
|
22 |
#play-pause-btn.playing { background-color: var(--primary-color); }
|
23 |
#play-pause-btn.paused { background-color: var(--success-color); }
|
@@ -25,6 +25,7 @@
|
|
25 |
#refresh-btn { background-color: #ffc107; color: black; }
|
26 |
|
27 |
/* 개입 기능 선택 탭*/
|
|
|
28 |
#intervention-tabs { display: flex; flex-direction: column; gap: 10px;}
|
29 |
.tab-btn { padding: 8px 15px; border-radius: 6px; border: 1px solid var(--border-color); cursor: pointer; font-weight: 500; color: #495057; text-align: center;}
|
30 |
.tab-btn.active { background-color: var(--primary-color); color: white; border-color: var(--primary-color);}
|
@@ -62,6 +63,7 @@
|
|
62 |
<div id="intervention-tabs">
|
63 |
<button id="tab-event" class="tab-btn active">이벤트 주입</button>
|
64 |
<button id="tab-relation" class="tab-btn">관계 설정</button>
|
|
|
65 |
</div>
|
66 |
<!-- 개입 기능 패널 -->
|
67 |
<div id="intervention-panels">
|
@@ -84,6 +86,12 @@
|
|
84 |
</select>
|
85 |
<button id="force-relationship-btn">관계 설정</button>
|
86 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
</div>
|
88 |
</div>
|
89 |
</div>
|
@@ -124,6 +132,13 @@
|
|
124 |
const relationNpc2Select = document.getElementById('relation-npc2-select');
|
125 |
const relationshipTypeSelect = document.getElementById('relationship-type-select');
|
126 |
const forceRelationshipBtn = document.getElementById('force-relationship-btn');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
|
128 |
let network = null;
|
129 |
let nodesDataSet = new vis.DataSet([]);
|
@@ -143,12 +158,40 @@
|
|
143 |
panelEvent.classList.add('active');
|
144 |
tabRelation.classList.remove('active');
|
145 |
panelRelation.classList.remove('active');
|
|
|
|
|
146 |
});
|
147 |
tabRelation.addEventListener('click', () => {
|
|
|
|
|
148 |
tabRelation.classList.add('active');
|
149 |
panelRelation.classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
150 |
tabEvent.classList.remove('active');
|
151 |
panelEvent.classList.remove('active');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
});
|
153 |
|
154 |
async function updateWorld() {
|
@@ -207,7 +250,7 @@
|
|
207 |
}
|
208 |
|
209 |
function updateNpcSelect(npcNodes) {
|
210 |
-
const selections = [eventNpcSelect, relationNpc1Select, relationNpc2Select];
|
211 |
selections.forEach(select => {
|
212 |
const currentVal = select.value;
|
213 |
select.innerHTML = '';
|
|
|
14 |
#network { width: 100%; flex: 1; border-bottom: 1px solid var(--border-color); }
|
15 |
|
16 |
/* 전체 컨트롤 패널 (가장 바깥쪽) */
|
17 |
+
#controls { padding: 15px; display: flex; flex-direction: column; gap: 15px; background-color: #3e3f41; border-radius: 0 0 12px 12px;}
|
18 |
|
19 |
/* 메인 컨트롤 버튼 */
|
20 |
+
#main-controls { display: flex; gap: 10px; justify-content: flex-start;}
|
21 |
#main-controls button { padding: 8px 15px; border-radius: 6px; border: none; cursor: pointer; color: rgb(0, 0, 0); font-size: 14px; font-weight: 500; transition: background-color 0.2s;}
|
22 |
#play-pause-btn.playing { background-color: var(--primary-color); }
|
23 |
#play-pause-btn.paused { background-color: var(--success-color); }
|
|
|
25 |
#refresh-btn { background-color: #ffc107; color: black; }
|
26 |
|
27 |
/* 개입 기능 선택 탭*/
|
28 |
+
#intervention-area { display: flex; gap: 15px; align-items: flex-start;}
|
29 |
#intervention-tabs { display: flex; flex-direction: column; gap: 10px;}
|
30 |
.tab-btn { padding: 8px 15px; border-radius: 6px; border: 1px solid var(--border-color); cursor: pointer; font-weight: 500; color: #495057; text-align: center;}
|
31 |
.tab-btn.active { background-color: var(--primary-color); color: white; border-color: var(--primary-color);}
|
|
|
63 |
<div id="intervention-tabs">
|
64 |
<button id="tab-event" class="tab-btn active">이벤트 주입</button>
|
65 |
<button id="tab-relation" class="tab-btn">관계 설정</button>
|
66 |
+
<button id="tab-orchestrate" class="tab-btn">상황 연출</button>
|
67 |
</div>
|
68 |
<!-- 개입 기능 패널 -->
|
69 |
<div id="intervention-panels">
|
|
|
86 |
</select>
|
87 |
<button id="force-relationship-btn">관계 설정</button>
|
88 |
</div>
|
89 |
+
<div id="panel-orchestrate" class="control-panel">
|
90 |
+
<select id="orch-npc1-select" title="대화를 시작할 NPC"></select>
|
91 |
+
<select id="orch-npc2-select" title="대화 상대 NPC"></select>
|
92 |
+
<input type="text" id="situation-text-input" placeholder="대화 주제 또는 상황">
|
93 |
+
<button id="orchestrate-btn">대화 시작</button>
|
94 |
+
</div>
|
95 |
</div>
|
96 |
</div>
|
97 |
</div>
|
|
|
132 |
const relationNpc2Select = document.getElementById('relation-npc2-select');
|
133 |
const relationshipTypeSelect = document.getElementById('relationship-type-select');
|
134 |
const forceRelationshipBtn = document.getElementById('force-relationship-btn');
|
135 |
+
// 대화 시작하기
|
136 |
+
const tabOrchestrate = document.getElementById('tab-orchestrate');
|
137 |
+
const panelOrchestrate = document.getElementById('panel-orchestrate');
|
138 |
+
const orchNpc1Select = document.getElementById('orch-npc1-select');
|
139 |
+
const orchNpc2Select = document.getElementById('orch-npc2-select');
|
140 |
+
const situationTextInput = document.getElementById('situation-text-input');
|
141 |
+
const orchestrateBtn = document.getElementById('orchestrate-btn');
|
142 |
|
143 |
let network = null;
|
144 |
let nodesDataSet = new vis.DataSet([]);
|
|
|
158 |
panelEvent.classList.add('active');
|
159 |
tabRelation.classList.remove('active');
|
160 |
panelRelation.classList.remove('active');
|
161 |
+
tabOrchestrate.classList.remove('active');
|
162 |
+
panelOrchestrate.classList.remove('active');
|
163 |
});
|
164 |
tabRelation.addEventListener('click', () => {
|
165 |
+
tabEvent.classList.remove('active');
|
166 |
+
panelEvent.classList.remove('active');
|
167 |
tabRelation.classList.add('active');
|
168 |
panelRelation.classList.add('active');
|
169 |
+
tabOrchestrate.classList.remove('active');
|
170 |
+
panelOrchestrate.classList.remove('active');
|
171 |
+
});
|
172 |
+
// 상황 연출 탭 클릭 이벤트
|
173 |
+
tabOrchestrate.addEventListener('click', () => {
|
174 |
tabEvent.classList.remove('active');
|
175 |
panelEvent.classList.remove('active');
|
176 |
+
tabRelation.classList.remove('active');
|
177 |
+
panelRelation.classList.remove('active');
|
178 |
+
tabOrchestrate.classList.add('active');
|
179 |
+
panelOrchestrate.classList.add('active');
|
180 |
+
});
|
181 |
+
|
182 |
+
// 대화 시작 버튼 이벤트 리스너 추가
|
183 |
+
orchestrateBtn.addEventListener('click', async () => {
|
184 |
+
const npc1_name = orchNpc1Select.value;
|
185 |
+
const npc2_name = orchNpc2Select.value;
|
186 |
+
const situation = situationTextInput.value;
|
187 |
+
if (!npc1_name || !npc2_name || !situation || npc1_name === npc2_name) {
|
188 |
+
return alert ('서로 다른 두 NPC와 상황을 모두 입력해주세요.');
|
189 |
+
}
|
190 |
+
await fetch('/npc_social_network/api/orchestrate_conversation', {
|
191 |
+
method: 'POST',
|
192 |
+
headers: { 'Content-Type': 'application/json' },
|
193 |
+
body: JSON.stringify({ npc1_name, npc2_name, situation })
|
194 |
+
});
|
195 |
});
|
196 |
|
197 |
async function updateWorld() {
|
|
|
250 |
}
|
251 |
|
252 |
function updateNpcSelect(npcNodes) {
|
253 |
+
const selections = [eventNpcSelect, relationNpc1Select, relationNpc2Select, orchNpc1Select, orchNpc2Select];
|
254 |
selections.forEach(select => {
|
255 |
const currentVal = select.value;
|
256 |
select.innerHTML = '';
|